2026-01-31 15:23:18 +05:30
// lib/screens/contribute_screen.dart
import ' dart:io ' ;
import ' ../core/app_decoration.dart ' ;
import ' package:flutter/foundation.dart ' show kIsWeb ;
import ' package:flutter/material.dart ' ;
import ' package:image_picker/image_picker.dart ' ;
class ContributeScreen extends StatefulWidget {
const ContributeScreen ( { Key ? key } ) : super ( key: key ) ;
@ override
State < ContributeScreen > createState ( ) = > _ContributeScreenState ( ) ;
}
class _ContributeScreenState extends State < ContributeScreen > with SingleTickerProviderStateMixin {
// Primary accent used for buttons / active tab (kept as a single constant)
static const Color _primary = Color ( 0xFF0B63D6 ) ;
// single corner radius to use everywhere
static const double _cornerRadius = 18.0 ;
// Form controllers
final TextEditingController _titleCtl = TextEditingController ( ) ;
final TextEditingController _locationCtl = TextEditingController ( ) ;
final TextEditingController _organizerCtl = TextEditingController ( ) ;
final TextEditingController _descriptionCtl = TextEditingController ( ) ;
DateTime ? _selectedDate ;
String _selectedCategory = ' Music ' ;
// Image pickers
final ImagePicker _picker = ImagePicker ( ) ;
XFile ? _coverImageFile ;
XFile ? _thumbImageFile ;
bool _submitting = false ;
// Tab state: 0 = Contribute, 1 = Leaderboard, 2 = Achievements
int _activeTab = 0 ;
// Example progress value (0..1)
double _progress = 0.45 ;
// A few category options
final List < String > _categories = [ ' Music ' , ' Food ' , ' Arts ' , ' Sports ' , ' Tech ' , ' Community ' ] ;
@ override
void dispose ( ) {
_titleCtl . dispose ( ) ;
_locationCtl . dispose ( ) ;
_organizerCtl . dispose ( ) ;
_descriptionCtl . dispose ( ) ;
super . dispose ( ) ;
}
Future < void > _pickCoverImage ( ) async {
try {
final XFile ? picked = await _picker . pickImage ( source : ImageSource . gallery , imageQuality: 85 ) ;
if ( picked ! = null ) setState ( ( ) = > _coverImageFile = picked ) ;
} catch ( e ) {
if ( mounted ) ScaffoldMessenger . of ( context ) . showSnackBar ( SnackBar ( content: Text ( ' Failed to pick image: $ e ' ) ) ) ;
}
}
Future < void > _pickThumbnailImage ( ) async {
try {
final XFile ? picked = await _picker . pickImage ( source : ImageSource . gallery , imageQuality: 85 ) ;
if ( picked ! = null ) setState ( ( ) = > _thumbImageFile = picked ) ;
} catch ( e ) {
if ( mounted ) ScaffoldMessenger . of ( context ) . showSnackBar ( SnackBar ( content: Text ( ' Failed to pick image: $ e ' ) ) ) ;
}
}
Future < void > _pickDate ( ) async {
final now = DateTime . now ( ) ;
final picked = await showDatePicker (
context: context ,
initialDate: _selectedDate ? ? now ,
firstDate: DateTime ( now . year - 2 ) ,
lastDate: DateTime ( now . year + 3 ) ,
builder: ( context , child ) {
return Theme (
data: Theme . of ( context ) . copyWith ( colorScheme: ColorScheme . light ( primary: _primary ) ) ,
child: child ! ,
) ;
} ,
) ;
if ( picked ! = null ) setState ( ( ) = > _selectedDate = picked ) ;
}
Future < void > _submit ( ) async {
if ( _titleCtl . text . trim ( ) . isEmpty ) {
ScaffoldMessenger . of ( context ) . showSnackBar ( const SnackBar ( content: Text ( ' Please enter an event title ' ) ) ) ;
return ;
}
setState ( ( ) = > _submitting = true ) ;
// simulate work
await Future . delayed ( const Duration ( milliseconds: 800 ) ) ;
setState ( ( ) = > _submitting = false ) ;
if ( mounted ) {
ScaffoldMessenger . of ( context ) . showSnackBar ( const SnackBar ( content: Text ( ' Submitted for verification (demo) ' ) ) ) ;
_clearForm ( ) ;
}
}
void _clearForm ( ) {
_titleCtl . clear ( ) ;
_locationCtl . clear ( ) ;
_organizerCtl . clear ( ) ;
_descriptionCtl . clear ( ) ;
_selectedDate = null ;
_selectedCategory = _categories . first ;
_coverImageFile = null ;
_thumbImageFile = null ;
setState ( ( ) { } ) ;
}
// ---------- UI Builders ----------
Widget _buildHeader ( BuildContext context ) {
final theme = Theme . of ( context ) ;
// header uses AppDecoration.blueGradient for background (project-specific)
return Container (
width: double . infinity ,
padding: const EdgeInsets . fromLTRB ( 20 , 32 , 20 , 24 ) ,
decoration: AppDecoration . blueGradient . copyWith (
borderRadius: BorderRadius . only (
bottomLeft: Radius . circular ( _cornerRadius ) ,
bottomRight: Radius . circular ( _cornerRadius ) ,
) ,
// subtle shadow only
boxShadow: [
BoxShadow ( color: Colors . black . withOpacity ( 0.06 ) , blurRadius: 8 , offset: const Offset ( 0 , 4 ) ) ,
] ,
) ,
child: SafeArea (
bottom: false ,
child: Column (
children: [
// increased spacing to create a breathable layout
const SizedBox ( height: 6 ) ,
// Title & subtitle (centered) — smaller title weight, clearer hierarchy
Text (
' Contributor Dashboard ' ,
textAlign: TextAlign . center ,
style: theme . textTheme . titleLarge ? . copyWith (
color: Colors . white ,
fontWeight: FontWeight . w700 ,
fontSize: 20 ,
) ,
) ,
const SizedBox ( height: 12 ) , // more space between title & subtitle
Text (
' Track your impact, earn rewards, and climb the ranks! ' ,
textAlign: TextAlign . center ,
style: theme . textTheme . bodyMedium ? . copyWith ( color: Colors . white . withOpacity ( 0.92 ) , fontSize: 13 , height: 1.35 ) ,
) ,
const SizedBox ( height: 20 ) , // more space before tabs
// Pill-style segmented tabs (animated active) — slimmer / minimal
_buildSegmentedTabs ( context ) ,
const SizedBox ( height: 18 ) , // comfortable spacing before contributor card
// Contributor level card — lighter, soft border, thinner progress
Container (
width: double . infinity ,
padding: const EdgeInsets . all ( 14 ) ,
margin: const EdgeInsets . symmetric ( horizontal: 4 ) ,
decoration: BoxDecoration (
color: Colors . white . withOpacity ( 0.08 ) , // slightly lighter than before
borderRadius: BorderRadius . circular ( _cornerRadius - 2 ) ,
border: Border . all ( color: Colors . white . withOpacity ( 0.09 ) ) , // soft border
) ,
child: Column (
crossAxisAlignment: CrossAxisAlignment . start ,
children: [
Text ( ' Contributor Level ' ,
style: theme . textTheme . titleSmall ? . copyWith ( color: Colors . white , fontWeight: FontWeight . w700 ) ) ,
const SizedBox ( height: 8 ) ,
Text ( ' Start earning rewards by contributing! ' , style: theme . textTheme . bodySmall ? . copyWith ( color: Colors . white70 ) ) ,
const SizedBox ( height: 12 ) ,
Row (
children: [
Expanded (
// animated progress using TweenAnimationBuilder
child: TweenAnimationBuilder < double > (
tween: Tween ( begin: 0.0 , end: _progress ) ,
duration: const Duration ( milliseconds: 700 ) ,
builder: ( context , value , _ ) = > ClipRRect (
borderRadius: BorderRadius . circular ( 6 ) ,
child: LinearProgressIndicator (
value: value ,
minHeight: 6 , // thinner progress bar
valueColor: const AlwaysStoppedAnimation < Color > ( Colors . white ) ,
backgroundColor: Colors . white24 ,
) ,
) ,
) ,
) ,
const SizedBox ( width: 12 ) ,
Container (
padding: const EdgeInsets . symmetric ( horizontal: 12 , vertical: 6 ) ,
decoration: BoxDecoration (
color: Colors . white . withOpacity ( 0.12 ) ,
borderRadius: BorderRadius . circular ( 12 ) ,
) ,
child: Text ( ' Explorer ' , style: theme . textTheme . bodySmall ? . copyWith ( color: Colors . white , fontWeight: FontWeight . w600 ) ) ,
) ,
] ,
) ,
const SizedBox ( height: 8 ) ,
Row (
mainAxisAlignment: MainAxisAlignment . spaceBetween ,
children: [
Text ( ' 30 pts ' , style: theme . textTheme . bodyMedium ? . copyWith ( color: Colors . white , fontWeight: FontWeight . w600 ) ) ,
Text ( ' Next: Enthusiast (50 pts) ' , style: theme . textTheme . bodyMedium ? . copyWith ( color: Colors . white70 ) ) ,
] ,
) ,
] ,
) ,
) ,
] ,
) ,
) ,
) ;
}
2026-03-14 09:08:34 +05:30
/// Bouncy spring curve matching web CSS: cubic-bezier(0.37, 1.95, 0.66, 0.56)
static const Curve _bouncyCurve = Cubic ( 0.37 , 1.95 , 0.66 , 0.56 ) ;
/// Tab icons for each tab
static const List < IconData > _tabIcons = [
Icons . edit_outlined ,
Icons . emoji_events_outlined ,
Icons . workspace_premium_outlined ,
] ;
2026-01-31 15:23:18 +05:30
Widget _buildSegmentedTabs ( BuildContext context ) {
final tabs = [ ' Contribute ' , ' Leaderboard ' , ' Achievements ' ] ;
return Padding (
2026-03-14 09:08:34 +05:30
padding: const EdgeInsets . symmetric ( horizontal: 4 , vertical: 10 ) ,
child: LayoutBuilder (
builder: ( context , constraints ) {
final containerWidth = constraints . maxWidth ;
// 6px padding on each side of the container
const double containerPadding = 6.0 ;
final innerWidth = containerWidth - ( containerPadding * 2 ) ;
final tabWidth = innerWidth / tabs . length ;
return ClipRRect (
borderRadius: BorderRadius . circular ( 16 ) ,
child: Container (
height: 57 ,
decoration: BoxDecoration (
color: Colors . white . withOpacity ( 0.15 ) ,
borderRadius: BorderRadius . circular ( 16 ) ,
border: Border . all ( color: Colors . white . withOpacity ( 0.2 ) ) ,
) ,
child: Stack (
children: [
// ── Sliding glider ──
AnimatedPositioned (
duration: const Duration ( milliseconds: 500 ) ,
curve: _bouncyCurve ,
left: containerPadding + ( _activeTab * tabWidth ) ,
top: containerPadding ,
width: tabWidth ,
height: 57 - ( containerPadding * 2 ) ,
child: AnimatedContainer (
duration: const Duration ( milliseconds: 400 ) ,
curve: Curves . easeInOut ,
decoration: BoxDecoration (
color: Colors . white . withOpacity ( 0.95 ) ,
borderRadius: BorderRadius . circular ( 12 ) ,
boxShadow: [
BoxShadow (
color: Colors . black . withOpacity ( 0.10 ) ,
blurRadius: 15 ,
offset: const Offset ( 0 , 4 ) ,
) ,
BoxShadow (
color: Colors . black . withOpacity ( 0.08 ) ,
blurRadius: 3 ,
offset: const Offset ( 0 , 1 ) ,
) ,
] ,
) ,
) ,
2026-01-31 15:23:18 +05:30
) ,
2026-03-14 09:08:34 +05:30
// ── Tab labels ──
Padding (
padding: const EdgeInsets . all ( containerPadding ) ,
child: Row (
children: List . generate ( tabs . length , ( i ) {
final active = i = = _activeTab ;
return Expanded (
child: GestureDetector (
onTap: ( ) = > setState ( ( ) = > _activeTab = i ) ,
behavior: HitTestBehavior . opaque ,
child: SizedBox (
height: double . infinity ,
child: Row (
mainAxisAlignment: MainAxisAlignment . center ,
children: [
// Animated icon: only shows for active tab
AnimatedSize (
duration: const Duration ( milliseconds: 300 ) ,
curve: Curves . easeInOut ,
child: active
? Padding (
padding: const EdgeInsets . only ( right: 6 ) ,
child: Icon (
_tabIcons [ i ] ,
size: 15 ,
color: _primary ,
) ,
)
: const SizedBox . shrink ( ) ,
) ,
Flexible (
child: AnimatedDefaultTextStyle (
duration: const Duration ( milliseconds: 300 ) ,
curve: Curves . easeInOut ,
style: TextStyle (
color: active ? _primary : Colors . white . withOpacity ( 0.7 ) ,
fontWeight: FontWeight . w600 ,
fontSize: 14 ,
fontFamily: Theme . of ( context ) . textTheme . bodyMedium ? . fontFamily ,
) ,
child: Text (
tabs [ i ] ,
textAlign: TextAlign . center ,
overflow: TextOverflow . ellipsis ,
) ,
) ,
) ,
] ,
) ,
2026-01-31 15:23:18 +05:30
) ,
) ,
2026-03-14 09:08:34 +05:30
) ;
} ) ,
) ,
2026-01-31 15:23:18 +05:30
) ,
2026-03-14 09:08:34 +05:30
] ,
2026-01-31 15:23:18 +05:30
) ,
2026-03-14 09:08:34 +05:30
) ,
) ;
} ,
2026-01-31 15:23:18 +05:30
) ,
) ;
}
Widget _buildForm ( BuildContext ctx ) {
final theme = Theme . of ( ctx ) ;
return Container (
width: double . infinity ,
margin: const EdgeInsets . only ( top: 18 ) ,
padding: const EdgeInsets . fromLTRB ( 18 , 20 , 18 , 28 ) ,
decoration: BoxDecoration (
color: theme . scaffoldBackgroundColor ,
borderRadius: BorderRadius . circular ( _cornerRadius ) ,
boxShadow: [ BoxShadow ( color: Colors . black . withOpacity ( 0.03 ) , blurRadius: 12 , offset: const Offset ( 0 , 6 ) ) ] ,
) ,
child: Column (
crossAxisAlignment: CrossAxisAlignment . start ,
children: [
// sheet title
Center (
child: Column (
children: [
Text ( ' Contribute an Event ' , style: theme . textTheme . headlineSmall ? . copyWith ( fontWeight: FontWeight . bold ) ) ,
const SizedBox ( height: 8 ) ,
Text ( ' Share local events. Earn points for every verified submission! ' ,
textAlign: TextAlign . center , style: theme . textTheme . bodyMedium ? . copyWith ( color: theme . hintColor ) ) ,
] ,
) ,
) ,
const SizedBox ( height: 18 ) ,
// small helper button
Center (
child: OutlinedButton (
onPressed: ( ) {
ScaffoldMessenger . of ( context ) . showSnackBar ( const SnackBar ( content: Text ( ' Want to edit an existing event? (demo) ' ) ) ) ;
} ,
style: OutlinedButton . styleFrom (
side: BorderSide ( color: Colors . grey . shade300 ) ,
shape: RoundedRectangleBorder ( borderRadius: BorderRadius . circular ( _cornerRadius - 6 ) ) ,
padding: const EdgeInsets . symmetric ( horizontal: 18 , vertical: 12 ) ,
) ,
child: const Text ( ' Want to edit an existing event? ' ) ,
) ,
) ,
const SizedBox ( height: 18 ) ,
// Event Title
Text ( ' Event Title ' , style: theme . textTheme . labelLarge ) ,
const SizedBox ( height: 8 ) ,
_roundedTextField ( controller: _titleCtl , hint: ' e.g. Local Food Festival ' ) ,
const SizedBox ( height: 14 ) ,
// Category
Text ( ' Category ' , style: theme . textTheme . labelLarge ) ,
const SizedBox ( height: 8 ) ,
Container (
padding: const EdgeInsets . symmetric ( horizontal: 12 ) ,
decoration: BoxDecoration ( color: theme . cardColor , borderRadius: BorderRadius . circular ( _cornerRadius - 8 ) ) ,
child: DropdownButtonHideUnderline (
child: DropdownButton < String > (
value: _selectedCategory ,
isExpanded: true ,
items: _categories . map ( ( c ) = > DropdownMenuItem ( value: c , child: Text ( c ) ) ) . toList ( ) ,
onChanged: ( v ) = > setState ( ( ) = > _selectedCategory = v ? ? _selectedCategory ) ,
icon: const Icon ( Icons . keyboard_arrow_down ) ,
) ,
) ,
) ,
const SizedBox ( height: 14 ) ,
// Date
Text ( ' Date ' , style: theme . textTheme . labelLarge ) ,
const SizedBox ( height: 8 ) ,
GestureDetector (
onTap: _pickDate ,
child: Container (
height: 48 ,
padding: const EdgeInsets . symmetric ( horizontal: 12 ) ,
alignment: Alignment . centerLeft ,
decoration: BoxDecoration ( color: theme . cardColor , borderRadius: BorderRadius . circular ( _cornerRadius - 8 ) ) ,
child: Text ( _selectedDate = = null ? ' Select date ' : ' ${ _selectedDate ! . day } / ${ _selectedDate ! . month } / ${ _selectedDate ! . year } ' , style: theme . textTheme . bodyMedium ) ,
) ,
) ,
const SizedBox ( height: 14 ) ,
// Location
Text ( ' Location ' , style: theme . textTheme . labelLarge ) ,
const SizedBox ( height: 8 ) ,
_roundedTextField ( controller: _locationCtl , hint: ' e.g. City Park, Calicut ' ) ,
const SizedBox ( height: 14 ) ,
// Organizer
Text ( ' Organizer Name ' , style: theme . textTheme . labelLarge ) ,
const SizedBox ( height: 8 ) ,
_roundedTextField ( controller: _organizerCtl , hint: ' Individual or Organization Name ' ) ,
const SizedBox ( height: 14 ) ,
// Description
Text ( ' Description ' , style: theme . textTheme . labelLarge ) ,
const SizedBox ( height: 8 ) ,
TextField (
controller: _descriptionCtl ,
minLines: 4 ,
maxLines: 6 ,
decoration: InputDecoration (
hintText: ' Tell us more about the event... ' ,
filled: true ,
fillColor: theme . cardColor ,
border: OutlineInputBorder ( borderRadius: BorderRadius . circular ( _cornerRadius - 8 ) , borderSide: BorderSide . none ) ,
contentPadding: const EdgeInsets . symmetric ( horizontal: 12 , vertical: 12 ) ,
) ,
) ,
const SizedBox ( height: 18 ) ,
// Event Images header
Text ( ' Event Images ' , style: theme . textTheme . titleMedium ? . copyWith ( fontWeight: FontWeight . w600 ) ) ,
const SizedBox ( height: 12 ) ,
// Cover image
Text ( ' Cover Image ' , style: theme . textTheme . bodySmall ) ,
const SizedBox ( height: 8 ) ,
GestureDetector (
onTap: _pickCoverImage ,
child: _imagePickerPlaceholder ( file: _coverImageFile , label: ' Cover Image ' ) ,
) ,
const SizedBox ( height: 12 ) ,
// Thumbnail image
Text ( ' Thumbnail ' , style: theme . textTheme . bodySmall ) ,
const SizedBox ( height: 8 ) ,
GestureDetector (
onTap: _pickThumbnailImage ,
child: _imagePickerPlaceholder ( file: _thumbImageFile , label: ' Thumbnail ' ) ,
) ,
const SizedBox ( height: 22 ) ,
// Submit button
SizedBox (
width: double . infinity ,
height: 52 ,
child: ElevatedButton (
onPressed: _submitting ? null : _submit ,
style: ElevatedButton . styleFrom (
backgroundColor: _primary ,
shape: RoundedRectangleBorder ( borderRadius: BorderRadius . circular ( _cornerRadius - 6 ) ) ,
) ,
child: _submitting
? const SizedBox ( width: 22 , height: 22 , child: CircularProgressIndicator ( color: Colors . white , strokeWidth: 2.2 ) )
: const Text ( ' Submit for Verification ' , style: TextStyle ( fontWeight: FontWeight . w600 ) ) ,
) ,
) ,
] ,
) ,
) ;
}
Widget _roundedTextField ( { required TextEditingController controller , required String hint } ) {
final theme = Theme . of ( context ) ;
return TextField (
controller: controller ,
decoration: InputDecoration (
hintText: hint ,
filled: true ,
fillColor: theme . cardColor ,
border: OutlineInputBorder ( borderRadius: BorderRadius . circular ( _cornerRadius - 8 ) , borderSide: BorderSide . none ) ,
contentPadding: const EdgeInsets . symmetric ( horizontal: 12 , vertical: 14 ) ,
) ,
) ;
}
Widget _imagePickerPlaceholder ( { XFile ? file , required String label } ) {
final theme = Theme . of ( context ) ;
if ( file = = null ) {
return Container (
width: double . infinity ,
height: 120 ,
decoration: BoxDecoration ( color: theme . cardColor , borderRadius: BorderRadius . circular ( _cornerRadius - 8 ) ) ,
child: Center (
child: Column (
mainAxisSize: MainAxisSize . min ,
children: [
Icon ( Icons . image , size: 28 , color: theme . hintColor ) ,
const SizedBox ( height: 8 ) ,
Text ( label , style: theme . textTheme . bodySmall ? . copyWith ( color: theme . hintColor ) ) ,
] ,
) ,
) ,
) ;
}
// show picked image (file or network depending on platform)
if ( kIsWeb | | file . path . startsWith ( ' http ' ) ) {
return ClipRRect (
borderRadius: BorderRadius . circular ( _cornerRadius - 8 ) ,
child: Image . network ( file . path , width: double . infinity , height: 120 , fit: BoxFit . cover , errorBuilder: ( _ , __ , ___ ) {
return Container (
width: double . infinity ,
height: 120 ,
decoration: BoxDecoration ( color: theme . cardColor , borderRadius: BorderRadius . circular ( _cornerRadius - 8 ) ) ,
child: Center ( child: Text ( label , style: theme . textTheme . bodySmall ? . copyWith ( color: theme . hintColor ) ) ) ,
) ;
} ) ,
) ;
} else {
final f = File ( file . path ) ;
if ( ! f . existsSync ( ) ) {
return Container (
width: double . infinity ,
height: 120 ,
decoration: BoxDecoration ( color: theme . cardColor , borderRadius: BorderRadius . circular ( _cornerRadius - 8 ) ) ,
child: Center ( child: Text ( label , style: theme . textTheme . bodySmall ? . copyWith ( color: theme . hintColor ) ) ) ,
) ;
}
return ClipRRect (
borderRadius: BorderRadius . circular ( _cornerRadius - 8 ) ,
child: Image . file ( f , width: double . infinity , height: 120 , fit: BoxFit . cover , errorBuilder: ( _ , __ , ___ ) {
return Container (
width: double . infinity ,
height: 120 ,
decoration: BoxDecoration ( color: theme . cardColor , borderRadius: BorderRadius . circular ( _cornerRadius - 8 ) ) ,
child: Center ( child: Text ( label , style: theme . textTheme . bodySmall ? . copyWith ( color: theme . hintColor ) ) ) ,
) ;
} ) ,
) ;
}
}
2026-03-14 13:57:34 +05:30
// ── Leaderboard state ──
int _leaderboardTimeFilter = 0 ; // 0 = All Time, 1 = This Month
int _leaderboardDistrictFilter = 0 ; // index into _districts
static const List < String > _districts = [
' Overall Kerala ' ,
' Thiruvananthapuram ' ,
' Kollam ' ,
' Pathanamthitta ' ,
' Alappuzha ' ,
' Kottayam ' ,
' Idukki ' ,
' Ernakulam ' ,
' Thrissur ' ,
' Palakkad ' ,
' Malappuram ' ,
' Kozhikode ' ,
' Wayanad ' ,
' Kannur ' ,
' Kasaragod ' ,
] ;
// Demo leaderboard data
static const List < Map < String , dynamic > > _leaderboardData = [
{ ' name ' : ' Annette Black ' , ' points ' : 4628 , ' level ' : ' Legend ' , ' events ' : 156 } ,
{ ' name ' : ' Jerome Bell ' , ' points ' : 4518 , ' level ' : ' Legend ' , ' events ' : 152 } ,
{ ' name ' : ' Theresa Webb ' , ' points ' : 4368 , ' level ' : ' Legend ' , ' events ' : 148 } ,
{ ' name ' : ' Courtney Henry ' , ' points ' : 4279 , ' level ' : ' Legend ' , ' events ' : 149 } ,
{ ' name ' : ' Cameron Williamson ' , ' points ' : 4150 , ' level ' : ' Legend ' , ' events ' : 144 } ,
{ ' name ' : ' Brooklyn Simmons ' , ' points ' : 4033 , ' level ' : ' Legend ' , ' events ' : 139 } ,
{ ' name ' : ' Leslie Alexander ' , ' points ' : 3914 , ' level ' : ' Champion ' , ' events ' : 134 } ,
{ ' name ' : ' Jenny Wilson ' , ' points ' : 3783 , ' level ' : ' Champion ' , ' events ' : 132 } ,
] ;
// Demo achievements data
static const List < Map < String , dynamic > > _achievementsData = [
{ ' name ' : ' Newcomer ' , ' subtitle ' : ' First Event Posted ' , ' icon ' : Icons . star_outline , ' color ' : 0xFFDBEAFE , ' iconColor ' : 0xFF3B82F6 , ' unlocked ' : true } ,
{ ' name ' : ' Contributor ' , ' subtitle ' : ' 10th Event Posted within a month ' , ' icon ' : Icons . workspace_premium , ' color ' : 0xFFFEF9C3 , ' iconColor ' : 0xFFEAB308 , ' unlocked ' : true } ,
{ ' name ' : ' On Fire! ' , ' subtitle ' : ' 3 Day Streak of logging in ' , ' icon ' : Icons . local_fire_department_outlined , ' color ' : 0xFFFFEDD5 , ' iconColor ' : 0xFFF97316 , ' unlocked ' : true , ' progress ' : 0.67 } ,
{ ' name ' : ' Verified ' , ' subtitle ' : ' Identity Verified successfully ' , ' icon ' : Icons . verified_outlined , ' color ' : 0xFFDCFCE7 , ' iconColor ' : 0xFF22C55E , ' unlocked ' : true } ,
{ ' name ' : ' Quality ' , ' subtitle ' : ' 5 Star Event Rating received ' , ' icon ' : Icons . lock_outline , ' color ' : 0xFFF1F5F9 , ' iconColor ' : 0xFF94A3B8 , ' unlocked ' : false } ,
{ ' name ' : ' Community ' , ' subtitle ' : ' Referred 5 Friends to the platform ' , ' icon ' : Icons . people_outline , ' color ' : 0xFFE0E7FF , ' iconColor ' : 0xFF6366F1 , ' unlocked ' : true , ' progress ' : 0.40 } ,
{ ' name ' : ' Expert ' , ' subtitle ' : ' Level 10 Reached in 3 months ' , ' icon ' : Icons . lock_outline , ' color ' : 0xFFF1F5F9 , ' iconColor ' : 0xFF94A3B8 , ' unlocked ' : false } ,
{ ' name ' : ' Precision ' , ' subtitle ' : ' 100% Data Accuracy on all events ' , ' icon ' : Icons . lock_outline , ' color ' : 0xFFF1F5F9 , ' iconColor ' : 0xFF94A3B8 , ' unlocked ' : false } ,
] ;
Widget _buildLeaderboard ( BuildContext context ) {
final theme = Theme . of ( context ) ;
return Container (
width: double . infinity ,
margin: const EdgeInsets . only ( top: 18 ) ,
padding: const EdgeInsets . fromLTRB ( 0 , 16 , 0 , 28 ) ,
child: Column (
crossAxisAlignment: CrossAxisAlignment . start ,
children: [
// ── Time filter: All Time / This Month ──
Padding (
padding: const EdgeInsets . symmetric ( horizontal: 16 ) ,
child: Row (
mainAxisAlignment: MainAxisAlignment . end ,
children: [
_buildTimeToggle ( theme ) ,
] ,
) ,
) ,
const SizedBox ( height: 12 ) ,
// ── District filter chips (horizontal scroll) ──
SizedBox (
height: 38 ,
child: ListView . separated (
scrollDirection: Axis . horizontal ,
padding: const EdgeInsets . symmetric ( horizontal: 16 ) ,
itemCount: _districts . length ,
separatorBuilder: ( _ , __ ) = > const SizedBox ( width: 8 ) ,
itemBuilder: ( context , i ) {
final active = i = = _leaderboardDistrictFilter ;
return GestureDetector (
onTap: ( ) = > setState ( ( ) = > _leaderboardDistrictFilter = i ) ,
child: AnimatedContainer (
duration: const Duration ( milliseconds: 250 ) ,
padding: const EdgeInsets . symmetric ( horizontal: 16 , vertical: 8 ) ,
decoration: BoxDecoration (
color: active ? _primary : Colors . white ,
borderRadius: BorderRadius . circular ( 20 ) ,
border: Border . all ( color: active ? _primary : Colors . grey . shade300 ) ,
) ,
child: Text (
_districts [ i ] ,
style: TextStyle (
color: active ? Colors . white : Colors . grey . shade600 ,
fontWeight: FontWeight . w600 ,
fontSize: 13 ,
) ,
) ,
) ,
) ;
} ,
) ,
) ,
const SizedBox ( height: 24 ) ,
// ── Podium (top 3) ──
_buildPodium ( theme ) ,
const SizedBox ( height: 24 ) ,
// ── Leaderboard table (rank 4+) ──
Padding (
padding: const EdgeInsets . symmetric ( horizontal: 16 ) ,
child: Column (
children: [
// Header
Padding (
padding: const EdgeInsets . only ( bottom: 12 ) ,
child: Row (
children: [
SizedBox ( width: 32 , child: Text ( ' RANK ' , style: TextStyle ( fontSize: 10 , fontWeight: FontWeight . w700 , color: Colors . grey . shade500 ) ) ) ,
const SizedBox ( width: 8 ) ,
Expanded ( child: Text ( ' USER ' , style: TextStyle ( fontSize: 10 , fontWeight: FontWeight . w700 , color: Colors . grey . shade500 ) ) ) ,
SizedBox ( width: 60 , child: Text ( ' POINTS ' , style: TextStyle ( fontSize: 10 , fontWeight: FontWeight . w700 , color: Colors . grey . shade500 ) ) ) ,
const SizedBox ( width: 8 ) ,
SizedBox ( width: 68 , child: Text ( ' LEVEL ' , style: TextStyle ( fontSize: 10 , fontWeight: FontWeight . w700 , color: Colors . grey . shade500 ) , textAlign: TextAlign . center ) ) ,
const SizedBox ( width: 8 ) ,
SizedBox ( width: 32 , child: Text ( ' EVENTS ' , style: TextStyle ( fontSize: 9 , fontWeight: FontWeight . w700 , color: Colors . grey . shade500 ) , textAlign: TextAlign . center ) ) ,
] ,
) ,
) ,
// Rows (rank 4+)
. . . List . generate (
_leaderboardData . length - 3 ,
( i ) = > _buildLeaderboardRow ( theme , i + 3 ) ,
) ,
] ,
) ,
) ,
] ,
) ,
) ;
}
Widget _buildTimeToggle ( ThemeData theme ) {
final labels = [ ' All Time ' , ' This Month ' ] ;
return Container (
padding: const EdgeInsets . all ( 3 ) ,
decoration: BoxDecoration (
color: Colors . grey . shade100 ,
borderRadius: BorderRadius . circular ( 24 ) ,
) ,
child: Row (
mainAxisSize: MainAxisSize . min ,
children: List . generate ( labels . length , ( i ) {
final active = i = = _leaderboardTimeFilter ;
return GestureDetector (
onTap: ( ) = > setState ( ( ) = > _leaderboardTimeFilter = i ) ,
child: AnimatedContainer (
duration: const Duration ( milliseconds: 250 ) ,
padding: const EdgeInsets . symmetric ( horizontal: 16 , vertical: 8 ) ,
decoration: BoxDecoration (
color: active ? _primary : Colors . transparent ,
borderRadius: BorderRadius . circular ( 20 ) ,
) ,
child: Text (
labels [ i ] ,
style: TextStyle (
color: active ? Colors . white : Colors . grey . shade600 ,
fontWeight: FontWeight . w600 ,
fontSize: 13 ,
) ,
) ,
) ,
) ;
} ) ,
) ,
) ;
}
Widget _buildPodium ( ThemeData theme ) {
if ( _leaderboardData . length < 3 ) return const SizedBox . shrink ( ) ;
final first = _leaderboardData [ 0 ] ; // #1
final second = _leaderboardData [ 1 ] ; // #2
final third = _leaderboardData [ 2 ] ; // #3
// Podium colors
const goldColor = Color ( 0xFFFBBF24 ) ;
const silverColor = Color ( 0xFFD1D5DB ) ;
const bronzeColor = Color ( 0xFFF97316 ) ;
Widget podiumSlot ( Map < String , dynamic > user , int rank , Color pillarColor , double pillarHeight , Color badgeColor ) {
return Column (
mainAxisAlignment: MainAxisAlignment . end ,
children: [
// Avatar with rank badge
Stack (
clipBehavior: Clip . none ,
children: [
CircleAvatar (
radius: rank = = 1 ? 32 : 26 ,
backgroundColor: badgeColor . withOpacity ( 0.2 ) ,
child: Icon ( Icons . person , size: rank = = 1 ? 32 : 26 , color: badgeColor ) ,
) ,
Positioned (
right: - 2 ,
bottom: - 2 ,
child: Container (
width: 20 ,
height: 20 ,
decoration: BoxDecoration (
color: badgeColor ,
shape: BoxShape . circle ,
border: Border . all ( color: Colors . white , width: 2 ) ,
) ,
alignment: Alignment . center ,
child: Text ( ' $ rank ' , style: const TextStyle ( color: Colors . white , fontSize: 10 , fontWeight: FontWeight . w800 ) ) ,
) ,
) ,
] ,
) ,
const SizedBox ( height: 6 ) ,
Text (
user [ ' name ' ] as String ,
style: const TextStyle ( fontWeight: FontWeight . w700 , fontSize: 12 ) ,
textAlign: TextAlign . center ,
maxLines: 1 ,
overflow: TextOverflow . ellipsis ,
) ,
Text (
' ${ _formatNumber ( user [ ' points ' ] as int ) } pts ' ,
style: TextStyle ( color: _primary , fontWeight: FontWeight . w600 , fontSize: 12 ) ,
) ,
const SizedBox ( height: 6 ) ,
// Pillar
Container (
width: 80 ,
height: pillarHeight ,
decoration: BoxDecoration (
color: pillarColor ,
borderRadius: const BorderRadius . vertical ( top: Radius . circular ( 10 ) ) ,
) ,
) ,
] ,
) ;
}
return Padding (
padding: const EdgeInsets . symmetric ( horizontal: 24 ) ,
child: Row (
crossAxisAlignment: CrossAxisAlignment . end ,
children: [
// #2 – left
Expanded ( child: podiumSlot ( second , 2 , silverColor , 70 , Colors . grey . shade500 ) ) ,
const SizedBox ( width: 8 ) ,
// #1 – center (tallest)
Expanded ( child: podiumSlot ( first , 1 , goldColor , 100 , goldColor ) ) ,
const SizedBox ( width: 8 ) ,
// #3 – right
Expanded ( child: podiumSlot ( third , 3 , bronzeColor , 55 , bronzeColor ) ) ,
] ,
) ,
) ;
}
Widget _buildLeaderboardRow ( ThemeData theme , int index ) {
final user = _leaderboardData [ index ] ;
final rank = index + 1 ;
final level = user [ ' level ' ] as String ;
Color levelColor ;
Color levelBg ;
switch ( level ) {
case ' Legend ' :
levelColor = const Color ( 0xFF16A34A ) ;
levelBg = const Color ( 0xFFDCFCE7 ) ;
break ;
case ' Champion ' :
levelColor = const Color ( 0xFF9333EA ) ;
levelBg = const Color ( 0xFFF3E8FF ) ;
break ;
default :
levelColor = Colors . grey ;
levelBg = Colors . grey . shade100 ;
}
return Container (
margin: const EdgeInsets . only ( bottom: 2 ) ,
padding: const EdgeInsets . symmetric ( vertical: 12 , horizontal: 4 ) ,
decoration: BoxDecoration (
color: Colors . white ,
border: Border ( bottom: BorderSide ( color: Colors . grey . shade100 ) ) ,
) ,
child: Row (
children: [
SizedBox ( width: 32 , child: Text ( ' $ rank ' , style: TextStyle ( fontWeight: FontWeight . w700 , fontSize: 14 , color: Colors . grey . shade700 ) ) ) ,
const SizedBox ( width: 8 ) ,
CircleAvatar (
radius: 18 ,
backgroundColor: Colors . grey . shade200 ,
child: Icon ( Icons . person , size: 20 , color: Colors . grey . shade500 ) ,
) ,
const SizedBox ( width: 10 ) ,
Expanded (
child: Text (
user [ ' name ' ] as String ,
style: const TextStyle ( fontWeight: FontWeight . w600 , fontSize: 14 ) ,
overflow: TextOverflow . ellipsis ,
) ,
) ,
SizedBox (
width: 60 ,
child: Text (
' ${ _formatNumber ( user [ ' points ' ] as int ) } pts ' ,
style: TextStyle ( color: _primary , fontWeight: FontWeight . w600 , fontSize: 12 ) ,
) ,
) ,
const SizedBox ( width: 8 ) ,
Container (
width: 68 ,
padding: const EdgeInsets . symmetric ( vertical: 4 ) ,
decoration: BoxDecoration (
color: levelBg ,
borderRadius: BorderRadius . circular ( 12 ) ,
) ,
alignment: Alignment . center ,
child: Text ( level , style: TextStyle ( color: levelColor , fontWeight: FontWeight . w600 , fontSize: 11 ) ) ,
) ,
const SizedBox ( width: 8 ) ,
SizedBox (
width: 32 ,
child: Row (
mainAxisAlignment: MainAxisAlignment . center ,
children: [
Icon ( Icons . calendar_today , size: 10 , color: Colors . grey . shade400 ) ,
const SizedBox ( width: 2 ) ,
Text ( ' ${ user [ ' events ' ] } ' , style: TextStyle ( fontSize: 11 , color: Colors . grey . shade600 ) ) ,
] ,
) ,
) ,
] ,
) ,
) ;
}
String _formatNumber ( int n ) {
if ( n > = 1000 ) {
return ' ${ ( n / 1000 ) . toStringAsFixed ( n % 1000 = = 0 ? 0 : 0 ) } , ${ ( n % 1000 ) . toString ( ) . padLeft ( 3 , ' 0 ' ) } ' ;
}
return ' $ n ' ;
}
// ── Achievements Tab ──
Widget _buildAchievements ( BuildContext context ) {
final theme = Theme . of ( context ) ;
return Container (
width: double . infinity ,
margin: const EdgeInsets . only ( top: 18 ) ,
padding: const EdgeInsets . fromLTRB ( 16 , 20 , 16 , 28 ) ,
child: Column (
crossAxisAlignment: CrossAxisAlignment . start ,
children: [
Text (
' Your Badges ' ,
style: theme . textTheme . headlineSmall ? . copyWith ( fontWeight: FontWeight . bold ) ,
) ,
const SizedBox ( height: 16 ) ,
// Badge grid
GridView . builder (
shrinkWrap: true ,
physics: const NeverScrollableScrollPhysics ( ) ,
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount (
crossAxisCount: 2 ,
mainAxisSpacing: 12 ,
crossAxisSpacing: 12 ,
childAspectRatio: 1.05 ,
) ,
itemCount: _achievementsData . length ,
itemBuilder: ( context , i ) = > _buildBadgeCard ( theme , _achievementsData [ i ] ) ,
) ,
] ,
) ,
) ;
}
Widget _buildBadgeCard ( ThemeData theme , Map < String , dynamic > badge ) {
final isUnlocked = badge [ ' unlocked ' ] as bool ;
final progress = badge [ ' progress ' ] as double ? ;
final badgeColor = Color ( badge [ ' color ' ] as int ) ;
final iconColor = Color ( badge [ ' iconColor ' ] as int ) ;
return Container (
padding: const EdgeInsets . all ( 14 ) ,
decoration: BoxDecoration (
color: Colors . white ,
borderRadius: BorderRadius . circular ( 16 ) ,
border: Border . all ( color: Colors . grey . shade100 ) ,
boxShadow: [
BoxShadow ( color: Colors . black . withOpacity ( 0.03 ) , blurRadius: 8 , offset: const Offset ( 0 , 2 ) ) ,
] ,
) ,
child: Column (
crossAxisAlignment: CrossAxisAlignment . start ,
children: [
// Icon circle
Container (
width: 44 ,
height: 44 ,
decoration: BoxDecoration (
color: badgeColor ,
shape: BoxShape . circle ,
) ,
child: Icon (
badge [ ' icon ' ] as IconData ,
color: iconColor ,
size: 22 ,
) ,
) ,
const Spacer ( ) ,
// Name + lock indicator
Row (
children: [
Expanded (
child: Text (
badge [ ' name ' ] as String ,
style: TextStyle (
fontWeight: FontWeight . w700 ,
fontSize: 14 ,
color: isUnlocked ? Colors . black87 : Colors . grey . shade400 ,
) ,
maxLines: 1 ,
overflow: TextOverflow . ellipsis ,
) ,
) ,
if ( ! isUnlocked )
Icon ( Icons . lock_outline , size: 14 , color: Colors . grey . shade400 ) ,
] ,
) ,
const SizedBox ( height: 4 ) ,
Text (
badge [ ' subtitle ' ] as String ,
style: TextStyle (
fontSize: 11 ,
color: isUnlocked ? Colors . grey . shade600 : Colors . grey . shade400 ,
height: 1.3 ,
) ,
maxLines: 2 ,
overflow: TextOverflow . ellipsis ,
) ,
// Progress bar if applicable
if ( progress ! = null ) . . . [
const SizedBox ( height: 8 ) ,
ClipRRect (
borderRadius: BorderRadius . circular ( 4 ) ,
child: TweenAnimationBuilder < double > (
tween: Tween ( begin: 0 , end: progress ) ,
duration: const Duration ( milliseconds: 800 ) ,
builder: ( _ , val , __ ) = > LinearProgressIndicator (
value: val ,
minHeight: 5 ,
valueColor: AlwaysStoppedAnimation < Color > ( _primary ) ,
backgroundColor: Colors . grey . shade200 ,
) ,
) ,
) ,
const SizedBox ( height: 4 ) ,
Align (
alignment: Alignment . centerRight ,
child: Text (
' ${ ( progress * 100 ) . toInt ( ) } % ' ,
style: TextStyle ( fontSize: 11 , color: Colors . grey . shade500 , fontWeight: FontWeight . w600 ) ,
) ,
) ,
] ,
] ,
) ,
) ;
}
2026-01-31 15:23:18 +05:30
@ override
Widget build ( BuildContext context ) {
2026-03-14 13:57:34 +05:30
// Switch content based on active tab
Widget tabContent ;
switch ( _activeTab ) {
case 1 :
tabContent = _buildLeaderboard ( context ) ;
break ;
case 2 :
tabContent = _buildAchievements ( context ) ;
break ;
default :
tabContent = Padding (
padding: const EdgeInsets . symmetric ( horizontal: 12 ) ,
child: _buildForm ( context ) ,
) ;
}
2026-01-31 15:23:18 +05:30
return Scaffold (
backgroundColor: Theme . of ( context ) . scaffoldBackgroundColor ,
body: SafeArea (
bottom: false ,
child: SingleChildScrollView (
physics: const BouncingScrollPhysics ( ) ,
child: Column (
children: [
_buildHeader ( context ) ,
2026-03-14 13:57:34 +05:30
AnimatedSwitcher (
duration: const Duration ( milliseconds: 300 ) ,
child: KeyedSubtree (
key: ValueKey < int > ( _activeTab ) ,
child: tabContent ,
) ,
2026-01-31 15:23:18 +05:30
) ,
const SizedBox ( height: 36 ) ,
] ,
) ,
) ,
) ,
) ;
}
}