2026-01-31 15:23:18 +05:30
import ' dart:async ' ;
import ' package:flutter/material.dart ' ;
import ' package:shared_preferences/shared_preferences.dart ' ;
import ' ../features/events/models/event_models.dart ' ;
import ' ../features/events/services/events_service.dart ' ;
import ' calendar_screen.dart ' ;
import ' profile_screen.dart ' ;
import ' booking_screen.dart ' ;
import ' settings_screen.dart ' ;
import ' learn_more_screen.dart ' ;
release: bump version to 1.4(p) (versionCode 14)
- Update versionCode 12 → 14, versionName 1.3(p) → 1.4(p)
- Update pubspec.yaml version to 1.4.0+14
- Add CHANGELOG.md with full version history
- Update README.md: version badge + changelog section
- Desktop Contribute Dashboard rebuilt to match web version
- Contributor Dashboard title, 3-tab nav (Contribute/Leaderboard/Achievements)
- Two-column submit form, tier milestone progress bar
- Desktop leaderboard with podium, filters, rank table (green points)
- Desktop achievements 3-column badge grid
- Inline Reward Shop with RP balance
- Gamification feature module (EP, RP, leaderboard, achievements, shop)
- Profile screen redesigned to match web app layout with animations
- Home screen bottom sheet date filter chips
- Updated API endpoints, login/event detail screens, theme colors
- Added Gilroy font suite, responsive layout improvements
2026-03-18 11:10:56 +05:30
import ' contribute_screen.dart ' ;
2026-01-31 15:23:18 +05:30
import ' ../core/app_decoration.dart ' ;
release: bump version to 1.4(p) (versionCode 14)
- Update versionCode 12 → 14, versionName 1.3(p) → 1.4(p)
- Update pubspec.yaml version to 1.4.0+14
- Add CHANGELOG.md with full version history
- Update README.md: version badge + changelog section
- Desktop Contribute Dashboard rebuilt to match web version
- Contributor Dashboard title, 3-tab nav (Contribute/Leaderboard/Achievements)
- Two-column submit form, tier milestone progress bar
- Desktop leaderboard with podium, filters, rank table (green points)
- Desktop achievements 3-column badge grid
- Inline Reward Shop with RP balance
- Gamification feature module (EP, RP, leaderboard, achievements, shop)
- Profile screen redesigned to match web app layout with animations
- Home screen bottom sheet date filter chips
- Updated API endpoints, login/event detail screens, theme colors
- Added Gilroy font suite, responsive layout improvements
2026-03-18 11:10:56 +05:30
import ' ../features/gamification/providers/gamification_provider.dart ' ;
import ' package:provider/provider.dart ' ;
2026-01-31 15:23:18 +05:30
class HomeDesktopScreen extends StatefulWidget {
final bool skipSidebarEntranceAnimation ;
const HomeDesktopScreen ( { Key ? key , this . skipSidebarEntranceAnimation = false } ) : super ( key: key ) ;
@ override
State < HomeDesktopScreen > createState ( ) = > _HomeDesktopScreenState ( ) ;
}
class _HomeDesktopScreenState extends State < HomeDesktopScreen > with SingleTickerProviderStateMixin {
final EventsService _eventsService = EventsService ( ) ;
// Navigation state
int selectedMenu = 0 ; // 0 = Home, 1 = Calendar, 2 = Profile, 3 = Bookings, 4 = Contribute, 5 = Settings
// Backend-driven data for Home
List < EventModel > _events = [ ] ;
List < EventTypeModel > _types = [ ] ;
bool _loading = true ;
bool _loadingTypes = true ;
// Selection: either a backend id (event type) or a fixed label filter.
int _SelectedTypeId = - 1 ;
String ? _selectedTypeLabel ;
// fixed categories required by product
final List < String > _fixedCategories = [
' All Events ' ,
] ;
// User prefs
String _username = ' Guest ' ;
String _location = ' Unknown ' ;
String _pincode = ' all ' ;
String ? _profileImage ;
// Sidebar text animation
late final AnimationController _sidebarTextController ;
late final Animation < double > _sidebarTextOpacity ;
// Sidebar width constant (used when computing main content width)
static const double _sidebarWidth = 220 ;
// Topbar compact breakpoint (content width)
static const double _compactTopBarWidth = 720 ;
// --- marquee (featured events) fields ---
late final ScrollController _marqueeController ;
Timer ? _marqueeTimer ;
// Speed in pixels per second
double _marqueeSpeed = 40.0 ;
final Duration _marqueeTick = const Duration ( milliseconds: 16 ) ;
@ override
void initState ( ) {
super . initState ( ) ;
_sidebarTextController = AnimationController ( vsync: this , duration: const Duration ( milliseconds: 420 ) ) ;
_sidebarTextOpacity = Tween < double > ( begin: 0.0 , end: 1.0 ) . animate ( CurvedAnimation ( parent: _sidebarTextController , curve: Curves . easeOut ) ) ;
_marqueeController = ScrollController ( ) ;
if ( widget . skipSidebarEntranceAnimation ) {
_sidebarTextController . value = 1.0 ;
} else {
WidgetsBinding . instance . addPostFrameCallback ( ( _ ) {
if ( mounted ) _sidebarTextController . forward ( ) ;
} ) ;
}
// load initial data for home only
_loadPreferencesAndData ( ) ;
}
@ override
void dispose ( ) {
_sidebarTextController . dispose ( ) ;
_marqueeTimer ? . cancel ( ) ;
_marqueeController . dispose ( ) ;
super . dispose ( ) ;
}
// ------------------------ Data loaders ------------------------
Future < void > _loadPreferencesAndData ( ) async {
setState ( ( ) {
_loading = true ;
_loadingTypes = true ;
} ) ;
final prefs = await SharedPreferences . getInstance ( ) ;
// UI display name should come from display_name (profile). Backend identity is separate.
_username = prefs . getString ( ' display_name ' ) ? ? prefs . getString ( ' username ' ) ? ? ' Jane Doe ' ;
_location = prefs . getString ( ' location ' ) ? ? ' Whitefield, Bengaluru ' ;
_pincode = prefs . getString ( ' pincode ' ) ? ? ' all ' ;
_profileImage = prefs . getString ( ' profileImage ' ) ;
await Future . wait ( [
_fetchEventTypes ( ) ,
_fetchEventsByPincode ( _pincode ) ,
] ) ;
if ( mounted ) setState ( ( ) {
_loading = false ;
_loadingTypes = false ;
} ) ;
// start marquee when we have events
_restartMarquee ( ) ;
}
Future < void > _fetchEventTypes ( ) async {
try {
final types = await _eventsService . getEventTypes ( ) ;
if ( mounted ) setState ( ( ) = > _types = types ) ;
} catch ( e ) {
if ( mounted ) ScaffoldMessenger . of ( context ) . showSnackBar ( SnackBar ( content: Text ( ' Failed to load categories: ${ e . toString ( ) } ' ) ) ) ;
}
}
Future < void > _fetchEventsByPincode ( String pincode ) async {
try {
final events = await _eventsService . getEventsByPincode ( pincode ) ;
if ( mounted ) setState ( ( ) = > _events = events ) ;
} catch ( e ) {
if ( mounted ) ScaffoldMessenger . of ( context ) . showSnackBar ( SnackBar ( content: Text ( ' Failed to load events: ${ e . toString ( ) } ' ) ) ) ;
}
// ensure marquee restarts when event list changes
_restartMarquee ( ) ;
}
/// Public refresh entry used by UI (pull-to-refresh).
Future < void > _refreshHome ( ) async {
// Prevent duplicate refresh triggers
if ( _loading ) return ;
setState ( ( ) = > _loading = true ) ;
try {
await _loadPreferencesAndData ( ) ;
} finally {
// _loadPreferencesAndData normally clears _loading, but ensure we reset if needed.
if ( mounted & & _loading ) setState ( ( ) = > _loading = false ) ;
}
}
// ------------------------ Helpers ------------------------
String ? _chooseEventImage ( EventModel e ) {
if ( e . thumbImg ! = null & & e . thumbImg ! . trim ( ) . isNotEmpty ) return e . thumbImg ! . trim ( ) ;
if ( e . images . isNotEmpty & & e . images . first . image . trim ( ) . isNotEmpty ) return e . images . first . image . trim ( ) ;
return null ;
}
Widget _profileAvatar ( ) {
// If profile image exists and is a network URL -> show it
if ( _profileImage ! = null & & _profileImage ! . trim ( ) . isNotEmpty ) {
final url = _profileImage ! . trim ( ) ;
if ( url . startsWith ( ' http ' ) ) {
return CircleAvatar (
radius: 20 ,
backgroundColor: Colors . grey . shade200 ,
backgroundImage: NetworkImage ( url ) ,
onBackgroundImageError: ( _ , __ ) { } ,
child: const Icon ( Icons . person , color: Colors . transparent ) ,
) ;
}
}
// Fallback → initials (clean & readable)
final name = _username . trim ( ) ;
String initials = ' U ' ;
if ( name . isNotEmpty ) {
if ( name . contains ( ' @ ' ) ) {
// Email → first letter only
initials = name [ 0 ] . toUpperCase ( ) ;
} else {
final parts = name . split ( ' ' ) . where ( ( p ) = > p . isNotEmpty ) . toList ( ) ;
initials = parts . isEmpty ? ' U ' : parts . take ( 2 ) . map ( ( p ) = > p [ 0 ] . toUpperCase ( ) ) . join ( ) ;
}
}
return CircleAvatar (
radius: 20 ,
backgroundColor: Colors . blue . shade600 ,
child: Text (
initials ,
style: const TextStyle (
color: Colors . white ,
fontWeight: FontWeight . bold ,
) ,
) ,
) ;
}
// ------------------------ Marquee control ------------------------
void _restartMarquee ( ) {
// stop previous timer
_marqueeTimer ? . cancel ( ) ;
if ( _events . isEmpty ) {
// reset position
if ( _marqueeController . hasClients ) _marqueeController . jumpTo ( 0 ) ;
return ;
}
// Wait for next frame so scroll metrics are available (after the duplicated row is built)
WidgetsBinding . instance . addPostFrameCallback ( ( _ ) {
if ( ! mounted ) return ;
if ( ! _marqueeController . hasClients ) {
// Try again next frame
WidgetsBinding . instance . addPostFrameCallback ( ( _ ) = > _restartMarquee ( ) ) ;
return ;
}
final maxScroll = _marqueeController . position . maxScrollExtent ;
if ( maxScroll < = 0 ) {
// Nothing to scroll
return ;
}
// Start a periodic timer that increments offset smoothly
final tickSeconds = _marqueeTick . inMilliseconds / 1000.0 ;
final delta = _marqueeSpeed * tickSeconds ;
_marqueeTimer ? . cancel ( ) ;
_marqueeTimer = Timer . periodic ( _marqueeTick , ( _ ) {
if ( ! mounted | | ! _marqueeController . hasClients ) return ;
final cur = _marqueeController . offset ;
final max = _marqueeController . position . maxScrollExtent ;
final half = max / 2.0 ; // because we duplicate the list twice
double next = cur + delta ;
if ( next > = max ) {
// wrap back by half (seamless loop)
final wrapped = next - half ;
_marqueeController . jumpTo ( wrapped . clamp ( 0.0 , max ) ) ;
} else {
// small incremental jump gives smooth appearance
_marqueeController . jumpTo ( next ) ;
}
} ) ;
} ) ;
}
// ------------------------ Filtering logic ------------------------
List < EventModel > get _filteredEvents {
if ( _selectedTypeLabel ! = null & & _selectedTypeLabel ! . toLowerCase ( ) ! = ' all events ' ) {
final label = _selectedTypeLabel ! . toLowerCase ( ) ;
return _events . where ( ( e ) {
final nameFromBackend = ( e . eventTypeId ! = null & & e . eventTypeId ! > 0 )
? ( _types . firstWhere ( ( t ) = > t . id = = e . eventTypeId , orElse: ( ) = > EventTypeModel ( id: - 1 , name: ' ' , iconUrl: null ) ) . name )
: ' ' ;
final candidateNames = < String ? > [
e . venueName ,
e . title ,
e . name ,
nameFromBackend ,
] ;
return candidateNames . any ( ( c ) = > c ! = null & & c . toLowerCase ( ) . contains ( label ) ) ;
} ) . toList ( ) ;
}
if ( _SelectedTypeId ! = - 1 ) {
return _events . where ( ( e ) = > e . eventTypeId = = _SelectedTypeId ) . toList ( ) ;
}
return _events ;
}
void _selectBackendType ( int id ) {
setState ( ( ) {
_SelectedTypeId = id ;
_selectedTypeLabel = null ;
} ) ;
}
void _selectFixedLabel ( String label ) {
setState ( ( ) {
if ( label . toLowerCase ( ) = = ' all events ' ) {
_SelectedTypeId = - 1 ;
_selectedTypeLabel = null ;
} else {
_SelectedTypeId = - 1 ;
_selectedTypeLabel = label ;
}
} ) ;
}
// ------------------------ Left panel (nav) ------------------------
Widget _buildLeftPanel ( ) {
final theme = Theme . of ( context ) ;
// Layout: top area (logo + nav list) scrolls if necessary, bottom area (host panel + settings) remains pinned.
return Container (
width: _sidebarWidth ,
decoration: const BoxDecoration (
image: DecorationImage (
image: AssetImage ( ' assets/images/gradient_dark_blue.png ' ) ,
fit: BoxFit . cover ,
) ,
) ,
child: SafeArea (
child: Column (
children: [
const SizedBox ( height: 18 ) ,
FadeTransition (
opacity: _sidebarTextOpacity ,
child: Padding (
padding: const EdgeInsets . symmetric ( vertical: 8.0 ) ,
child: Padding (
padding: const EdgeInsets . symmetric ( horizontal: 18.0 ) ,
child: Text ( ' EVENTIFY ' , style: theme . textTheme . titleMedium ? . copyWith ( color: theme . colorScheme . onPrimary , fontWeight: FontWeight . bold , fontSize: 18 ) ) ,
) ,
) ,
) ,
const SizedBox ( height: 16 ) ,
// Expandable nav list
Expanded (
child: SingleChildScrollView (
physics: const ClampingScrollPhysics ( ) ,
child: Column (
crossAxisAlignment: CrossAxisAlignment . stretch ,
children: [
_navItem ( icon: Icons . home , label: ' Home ' , idx: 0 ) ,
_navItem ( icon: Icons . location_on , label: ' Events near you ' , idx: 0 ) ,
_navItem ( icon: Icons . calendar_today , label: ' Upcoming events ' , idx: 0 ) ,
_navItem ( icon: Icons . calendar_view_month , label: ' Calendar ' , idx: 1 ) ,
// <-- Profile between Calendar and Contribute
_navItem ( icon: Icons . person , label: ' Profile ' , idx: 2 ) ,
_navItem ( icon: Icons . add , label: ' Contribute ' , idx: 4 ) ,
const SizedBox ( height: 12 ) ,
// optionally more items...
] ,
) ,
) ,
) ,
// bottom pinned area
Padding (
padding: const EdgeInsets . symmetric ( horizontal: 12.0 ) ,
child: Column (
children: [
_buildHostPanel ( ) ,
const SizedBox ( height: 12 ) ,
_navItem ( icon: Icons . settings , label: ' Settings ' , idx: 5 ) ,
const SizedBox ( height: 12 ) ,
] ,
) ,
) ,
] ,
) ,
) ,
) ;
}
Widget _buildHostPanel ( ) {
final theme = Theme . of ( context ) ;
return Container (
width: double . infinity ,
padding: const EdgeInsets . all ( 12 ) ,
decoration: BoxDecoration (
image: const DecorationImage (
image: AssetImage ( ' assets/images/gradient_dark_blue.png ' ) ,
fit: BoxFit . cover ,
) ,
borderRadius: BorderRadius . circular ( 12 ) ,
) ,
child: Column (
crossAxisAlignment: CrossAxisAlignment . start ,
children: [
Text ( ' Hosting a private or ticketed event? ' , style: theme . textTheme . bodyMedium ? . copyWith ( color: theme . colorScheme . onPrimary , fontWeight: FontWeight . bold ) ) ,
const SizedBox ( height: 6 ) ,
Text ( ' Schedule a call back for setting up event. ' , style: theme . textTheme . bodySmall ? . copyWith ( color: theme . colorScheme . onPrimary . withOpacity ( 0.85 ) , fontSize: 12 ) ) ,
const SizedBox ( height: 10 ) ,
ElevatedButton ( onPressed: ( ) { } , style: ElevatedButton . styleFrom ( backgroundColor: theme . colorScheme . primary ) , child: Text ( ' Schedule Call ' , style: theme . textTheme . labelLarge ? . copyWith ( color: theme . colorScheme . onPrimary ) ) ) ,
] ,
) ,
) ;
}
Widget _navItem ( { required IconData icon , required String label , required int idx } ) {
final theme = Theme . of ( context ) ;
final selected = selectedMenu = = idx ;
final color = selected ? theme . colorScheme . onPrimary : theme . colorScheme . onPrimary . withOpacity ( 0.9 ) ;
return ListTile (
leading: Icon ( icon , color: color ) ,
title: Text ( label , style: TextStyle ( color: color ) ) ,
dense: true ,
onTap: ( ) {
setState ( ( ) {
selectedMenu = idx ;
} ) ;
if ( idx = = 0 ) _refreshHome ( ) ;
} ,
) ;
}
// ------------------------ Top bar (common, responsive) ------------------------
Widget _buildTopBar ( ) {
// The top bar sits inside the white content area (to the right of the sidebar).
// The search box is centered inside the content area (slightly toward the right because of fixed width).
final theme = Theme . of ( context ) ;
return LayoutBuilder ( builder: ( context , constraints ) {
final isCompact = constraints . maxWidth < _compactTopBarWidth ;
return Padding (
padding: const EdgeInsets . symmetric ( horizontal: 24.0 , vertical: 18 ) ,
child: Row (
children: [
// Left: location or icon when compact
if ( ! isCompact )
_fullLocationWidget ( )
else
IconButton (
tooltip: ' Location ' ,
onPressed: ( ) { } ,
icon: Icon ( Icons . location_on , color: theme . iconTheme . color ) ,
) ,
const SizedBox ( width: 12 ) ,
// Center: place the search bar centered in content area using Center + fixed width box.
if ( ! isCompact )
Expanded (
child: Align (
alignment: Alignment . center ,
child: Padding (
// ⬇️ Increase LEFT padding to move search bar more to the right
padding: const EdgeInsets . only ( left: 120 ) ,
child: SizedBox (
width: 520 ,
height: 44 ,
child: TextField (
decoration: InputDecoration (
filled: true ,
fillColor: theme . cardColor ,
prefixIcon: Icon ( Icons . search , color: theme . hintColor ) ,
hintText: ' Search events, pages, features... ' ,
hintStyle: theme . textTheme . bodyMedium ? . copyWith ( color: theme . hintColor ) ,
border: OutlineInputBorder (
borderRadius: BorderRadius . circular ( 12 ) ,
borderSide: BorderSide . none ,
) ,
) ,
style: theme . textTheme . bodyLarge ,
) ,
) ,
) ,
) ,
)
else
IconButton (
tooltip: ' Search ' ,
onPressed: ( ) { } ,
icon: Icon ( Icons . search , color: theme . iconTheme . color ) ,
) ,
// Spacer ensures right side stays right-aligned
const Spacer ( ) ,
// Right: notifications + (optionally username) + avatar
Row (
mainAxisSize: MainAxisSize . min ,
children: [
Stack (
children: [
IconButton ( onPressed: ( ) { } , icon: Icon ( Icons . notifications_none , color: theme . iconTheme . color ) ) ,
Positioned ( right: 6 , top: 6 , child: CircleAvatar ( radius: 8 , backgroundColor: Colors . red , child: Text ( ' 2 ' , style: theme . textTheme . bodySmall ? . copyWith ( color: Colors . white , fontSize: 10 ) ) ) ) ,
] ,
) ,
const SizedBox ( width: 8 ) ,
if ( ! isCompact ) . . . [
Column ( crossAxisAlignment: CrossAxisAlignment . start , children: [
Text ( _username , style: theme . textTheme . bodyLarge ? . copyWith ( fontWeight: FontWeight . bold ) ) ,
Text ( ' Explorer ' , style: theme . textTheme . bodySmall ? . copyWith ( color: theme . hintColor , fontSize: 12 ) ) ,
] ) ,
const SizedBox ( width: 8 ) ,
] ,
GestureDetector ( onTap: ( ) = > _openProfile ( ) , child: _profileAvatar ( ) ) ,
] ,
) ,
] ,
) ,
) ;
} ) ;
}
Widget _fullLocationWidget ( ) {
final theme = Theme . of ( context ) ;
return Column (
crossAxisAlignment: CrossAxisAlignment . start ,
children: [
Text ( ' Location ' , style: theme . textTheme . bodySmall ? . copyWith ( color: theme . hintColor , fontSize: 12 ) ) ,
const SizedBox ( height: 4 ) ,
Row ( children: [
Icon ( Icons . location_on , size: 18 , color: theme . hintColor ) ,
const SizedBox ( width: 6 ) ,
Text ( _location , style: theme . textTheme . bodyLarge ? . copyWith ( fontWeight: FontWeight . w600 ) ) ,
const SizedBox ( width: 6 ) ,
Icon ( Icons . arrow_drop_down , size: 18 , color: theme . hintColor ) ,
] ) ,
] ,
) ;
}
void _openProfile ( ) {
setState ( ( ) = > selectedMenu = 2 ) ;
}
// ------------------------ Home content (index 0) ------------------------
Widget _homeContent ( ) {
final theme = Theme . of ( context ) ;
// Entire home content is a vertical scrollable wrapped in RefreshIndicator for pull-to-refresh behaviour.
return RefreshIndicator (
onRefresh: _refreshHome ,
color: theme . colorScheme . primary ,
child: SingleChildScrollView (
physics: const AlwaysScrollableScrollPhysics ( ) ,
child: Column (
crossAxisAlignment: CrossAxisAlignment . start ,
children: [
const SizedBox ( height: 8 ) ,
// HERO / welcome panel
Padding (
padding: const EdgeInsets . symmetric ( horizontal: 24.0 ) ,
child: Container (
padding: const EdgeInsets . all ( 20 ) ,
decoration: AppDecoration . blueGradientRounded ( 16 ) ,
child: Row (
children: [
// left welcome
Expanded (
flex: 6 ,
child: Column ( crossAxisAlignment: CrossAxisAlignment . start , children: [
Text ( ' Welcome Back, ' , style: theme . textTheme . bodyMedium ? . copyWith ( color: Colors . white . withOpacity ( 0.9 ) , fontSize: 16 ) ) ,
const SizedBox ( height: 6 ) ,
Text ( _username , style: const TextStyle ( color: Colors . white , fontSize: 30 , fontWeight: FontWeight . bold ) ) ,
] ) ,
) ,
const SizedBox ( width: 16 ) ,
// right: horizontal featured events (backend-driven)
Expanded (
flex: 4 ,
child: SizedBox (
height: 90 ,
child: _events . isEmpty
? const SizedBox ( )
: _buildMarqueeFeaturedEvents ( ) ,
) ,
) ,
] ,
) ,
) ,
) ,
const SizedBox ( height: 18 ) ,
// Events header
Padding (
padding: const EdgeInsets . fromLTRB ( 24 , 0 , 24 , 8 ) ,
child: Row ( children: [
Expanded ( child: Text ( ' Events Around You ' , style: theme . textTheme . titleLarge ? . copyWith ( fontWeight: FontWeight . bold ) ) ) ,
TextButton ( onPressed: ( ) { } , child: Text ( ' View All ' , style: theme . textTheme . bodyMedium ) ) ,
] ) ,
) ,
// type chips: fixed categories first, then backend categories
if ( ! _loadingTypes ) SizedBox ( height: 56 , child: _buildTypeChips ( ) ) ,
const SizedBox ( height: 14 ) ,
// events area: use LayoutBuilder to decide between grid and horizontal scroll
Padding (
padding: const EdgeInsets . symmetric ( horizontal: 24 ) ,
child: LayoutBuilder (
builder: ( context , constraints ) {
// constraints.maxWidth is the available width inside the white content area (after padding)
return _buildEventsArea ( constraints . maxWidth ) ;
} ,
) ,
) ,
const SizedBox ( height: 40 ) ,
] ,
) ,
) ,
) ;
}
/// Build marquee (continuous leftward scroll) by duplicating the events list and using a ScrollController.
Widget _buildMarqueeFeaturedEvents ( ) {
// Duplicate events for seamless loop
final display = < EventModel > [ ] ;
display . addAll ( _events ) ;
display . addAll ( _events ) ;
return ClipRRect (
borderRadius: BorderRadius . circular ( 8 ) ,
child: SingleChildScrollView (
controller: _marqueeController ,
scrollDirection: Axis . horizontal ,
physics: const NeverScrollableScrollPhysics ( ) ,
child: Row (
children: List . generate ( display . length , ( i ) {
final e = display [ i ] ;
final img = _chooseEventImage ( e ) ;
return Padding (
padding: const EdgeInsets . only ( right: 12.0 ) ,
child: SizedBox ( width: 220 , child: _miniEventCard ( e , img ) ) ,
) ;
} ) ,
) ,
) ,
) ;
}
Widget _buildTypeChips ( ) {
final chips = < Widget > [ ] ;
// fixed categories first
for ( final name in _fixedCategories ) {
final isSelected = _selectedTypeLabel ! = null ? _selectedTypeLabel = = name : ( name = = ' All Events ' & & _SelectedTypeId = = - 1 & & _selectedTypeLabel = = null ) ;
chips . add ( _chipWidget (
label: name ,
onTap: ( ) = > _selectFixedLabel ( name ) ,
selected: isSelected ,
) ) ;
}
// then backend categories (if any)
for ( final t in _types ) {
final isSelected = ( _selectedTypeLabel = = null & & _SelectedTypeId = = t . id ) ;
chips . add ( _chipWidget ( label: t . name , onTap: ( ) = > _selectBackendType ( t . id ) , selected: isSelected ) ) ;
}
return ListView . separated (
padding: const EdgeInsets . symmetric ( horizontal: 24 ) ,
scrollDirection: Axis . horizontal ,
itemBuilder: ( _ , idx ) = > chips [ idx ] ,
separatorBuilder: ( _ , __ ) = > const SizedBox ( width: 8 ) ,
itemCount: chips . length ,
) ;
}
Widget _chipWidget ( {
required String label ,
required VoidCallback onTap ,
required bool selected ,
} ) {
final theme = Theme . of ( context ) ;
return InkWell (
borderRadius: BorderRadius . circular ( 10 ) ,
onTap: onTap ,
child: Container (
height: 40 , // 👈 fixed height for all chips
alignment: Alignment . center ,
padding: const EdgeInsets . symmetric ( horizontal: 16 ) ,
decoration: BoxDecoration (
color: selected ? theme . colorScheme . primary . withOpacity ( 0.08 ) : theme . cardColor ,
borderRadius: BorderRadius . circular ( 10 ) , // 👈 box shape (not pill)
border: Border . all (
color: selected ? theme . colorScheme . primary : theme . dividerColor ,
width: 1 ,
) ,
) ,
child: Text (
label ,
style: TextStyle (
fontSize: 14 ,
fontWeight: FontWeight . w600 ,
color: selected ? theme . colorScheme . primary : theme . textTheme . bodyLarge ? . color ,
) ,
) ,
) ,
) ;
}
// ------------------------
// EVENTS AREA: fixed 3-columns grid on wide widths; fallback horizontal on narrow widths.
// ------------------------
Widget _buildEventsArea ( double contentWidth ) {
// Preferred card width you'd like to keep (matches your reference)
const double preferredCardWidth = 360.0 ;
const double cardHeight = 220.0 ; // matches reference proportions
const double spacing = 18.0 ;
final list = _filteredEvents ;
final theme = Theme . of ( context ) ;
if ( _loading ) {
return Padding ( padding: const EdgeInsets . only ( top: 32 ) , child: Center ( child: CircularProgressIndicator ( color: theme . colorScheme . primary ) ) ) ;
}
if ( list . isEmpty ) {
return Padding ( padding: const EdgeInsets . symmetric ( vertical: 40 ) , child: Center ( child: Text ( ' No events available ' , style: theme . textTheme . bodyMedium ) ) ) ;
}
// Determine if we have space to show exactly 3 columns.
// Required width for 3 columns = 3 * preferredCardWidth + 2 * spacing
final double requiredWidthForThree = preferredCardWidth * 3 + spacing * 2 ;
if ( contentWidth > = requiredWidthForThree ) {
// Enough space: force a 3-column grid (this ensures exactly 3 cards per row)
return GridView . builder (
shrinkWrap: true ,
physics: const NeverScrollableScrollPhysics ( ) ,
itemCount: list . length ,
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount (
crossAxisCount: 3 , // always 3 when space allows
mainAxisExtent: cardHeight ,
crossAxisSpacing: spacing ,
mainAxisSpacing: spacing ,
) ,
itemBuilder: ( ctx , idx ) {
final e = list [ idx ] ;
final img = _chooseEventImage ( e ) ;
return _eventCardForGrid ( e , img ) ;
} ,
) ;
} else {
// Not enough space for 3 columns — use horizontal scroll preserving card width.
// This keeps behaviour sensible on narrower desktop windows.
final double preferredWidth = preferredCardWidth ;
return SizedBox (
height: cardHeight ,
child: ListView . separated (
scrollDirection: Axis . horizontal ,
itemCount: list . length ,
separatorBuilder: ( _ , __ ) = > const SizedBox ( width: spacing ) ,
itemBuilder: ( ctx , idx ) {
final e = list [ idx ] ;
final img = _chooseEventImage ( e ) ;
return SizedBox ( width: preferredWidth , child: _eventCardForFixedSize ( e , img , preferredWidth ) ) ;
} ,
) ,
) ;
}
}
// small horizontal card used inside HERO right area
Widget _miniEventCard ( EventModel e , String ? img ) {
final theme = Theme . of ( context ) ;
return Container (
width: 220 ,
padding: const EdgeInsets . all ( 8 ) ,
decoration: BoxDecoration ( color: theme . cardColor , borderRadius: BorderRadius . circular ( 12 ) ) ,
child: Row ( children: [
ClipRRect (
borderRadius: BorderRadius . circular ( 8 ) ,
child: img ! = null ? Image . network ( img , width: 64 , height: 64 , fit: BoxFit . cover ) : Container ( width: 64 , height: 64 , color: Theme . of ( context ) . dividerColor ) ,
) ,
const SizedBox ( width: 8 ) ,
Expanded (
child: Column ( mainAxisAlignment: MainAxisAlignment . center , crossAxisAlignment: CrossAxisAlignment . start , children: [
Text ( e . title ? ? e . name ? ? ' ' , maxLines: 1 , overflow: TextOverflow . ellipsis , style: Theme . of ( context ) . textTheme . bodyLarge ? . copyWith ( fontWeight: FontWeight . bold ) ) ,
const SizedBox ( height: 6 ) ,
Text ( e . startDate ? ? ' ' , style: Theme . of ( context ) . textTheme . bodySmall ? . copyWith ( color: Theme . of ( context ) . hintColor , fontSize: 12 ) ) ,
] ) ,
) ,
] ) ,
) ;
}
// ---------- Cards ----------
// Card used in grid (we can rely on grid cell having fixed height)
Widget _eventCardForGrid ( EventModel e , String ? img ) {
final theme = Theme . of ( context ) ;
// Styling constants to match reference
const double cardRadius = 16.0 ;
const double imageHeight = 140.0 ;
const double horizontalPadding = 12.0 ;
const double verticalPadding = 10.0 ;
// Friendly formatted date label
final dateLabel = ( e . startDate ! = null & & e . endDate ! = null & & e . startDate = = e . endDate )
? e . startDate !
: ( ( e . startDate ! = null & & e . endDate ! = null ) ? ' ${ e . startDate } - ${ e . endDate } ' : ( e . startDate ? ? ' ' ) ) ;
return GestureDetector (
onTap: ( ) = > Navigator . push ( context , MaterialPageRoute ( builder: ( _ ) = > LearnMoreScreen ( eventId: e . id ) ) ) ,
child: Container (
decoration: BoxDecoration (
color: theme . cardColor ,
borderRadius: BorderRadius . circular ( cardRadius ) ,
boxShadow: [
BoxShadow ( color: Colors . black . withOpacity ( 0.08 ) , blurRadius: 18 , offset: const Offset ( 0 , 10 ) ) ,
BoxShadow ( color: Colors . black . withOpacity ( 0.03 ) , blurRadius: 6 , offset: const Offset ( 0 , 3 ) ) ,
] ,
) ,
child: Column (
crossAxisAlignment: CrossAxisAlignment . start ,
children: [
// image flush to top corners
ClipRRect (
borderRadius: const BorderRadius . vertical ( top: Radius . circular ( cardRadius ) ) ,
child: img ! = null
? Image . network ( img , width: double . infinity , height: imageHeight , fit: BoxFit . cover , errorBuilder: ( _ , __ , ___ ) = > Container ( height: imageHeight , color: theme . dividerColor ) )
: Container ( height: imageHeight , width: double . infinity , color: theme . dividerColor ) ,
) ,
// content area
Padding (
padding: const EdgeInsets . fromLTRB ( horizontalPadding , verticalPadding , horizontalPadding , verticalPadding ) ,
child: Column (
crossAxisAlignment: CrossAxisAlignment . start ,
children: [
// Title (2 lines)
Text (
e . title ? ? e . name ? ? ' ' ,
maxLines: 2 ,
overflow: TextOverflow . ellipsis ,
style: theme . textTheme . bodyLarge ? . copyWith ( fontWeight: FontWeight . w700 , fontSize: 14 ) ,
) ,
const SizedBox ( height: 8 ) ,
// single row: date • location (icons small, subtle)
Row (
children: [
// calendar small badge
Container (
width: 18 ,
height: 18 ,
decoration: BoxDecoration ( color: theme . colorScheme . primary . withOpacity ( 0.12 ) , borderRadius: BorderRadius . circular ( 4 ) ) ,
child: Icon ( Icons . calendar_today , size: 12 , color: theme . colorScheme . primary ) ,
) ,
const SizedBox ( width: 8 ) ,
Expanded (
child: Text (
dateLabel ,
maxLines: 1 ,
overflow: TextOverflow . ellipsis ,
style: theme . textTheme . bodySmall ? . copyWith ( color: theme . hintColor , fontSize: 13 ) ,
) ,
) ,
const Padding (
padding: EdgeInsets . symmetric ( horizontal: 8.0 ) ,
child: Text ( ' • ' , style: TextStyle ( color: Colors . black26 ) ) ,
) ,
Container (
width: 18 ,
height: 18 ,
decoration: BoxDecoration ( color: theme . colorScheme . primary . withOpacity ( 0.12 ) , borderRadius: BorderRadius . circular ( 4 ) ) ,
child: Icon ( Icons . location_on , size: 12 , color: theme . colorScheme . primary ) ,
) ,
const SizedBox ( width: 8 ) ,
Expanded (
child: Text (
e . place ? ? ' ' ,
maxLines: 1 ,
overflow: TextOverflow . ellipsis ,
style: theme . textTheme . bodySmall ? . copyWith ( color: theme . hintColor , fontSize: 13 ) ,
) ,
) ,
] ,
) ,
] ,
) ,
) ,
] ,
) ,
) ,
) ;
}
// Card used in horizontal list (fixed width)
Widget _eventCardForFixedSize ( EventModel e , String ? img , double width ) {
final theme = Theme . of ( context ) ;
// Styling constants to match grid card
const double cardRadius = 16.0 ;
const double imageHeight = 140.0 ;
const double horizontalPadding = 12.0 ;
const double verticalPadding = 10.0 ;
final dateLabel = ( e . startDate ! = null & & e . endDate ! = null & & e . startDate = = e . endDate )
? e . startDate !
: ( ( e . startDate ! = null & & e . endDate ! = null ) ? ' ${ e . startDate } - ${ e . endDate } ' : ( e . startDate ? ? ' ' ) ) ;
return GestureDetector (
onTap: ( ) = > Navigator . push ( context , MaterialPageRoute ( builder: ( _ ) = > LearnMoreScreen ( eventId: e . id ) ) ) ,
child: Container (
width: width ,
decoration: BoxDecoration (
color: theme . cardColor ,
borderRadius: BorderRadius . circular ( cardRadius ) ,
boxShadow: [
BoxShadow ( color: Colors . black . withOpacity ( 0.08 ) , blurRadius: 18 , offset: const Offset ( 0 , 10 ) ) ,
BoxShadow ( color: Colors . black . withOpacity ( 0.03 ) , blurRadius: 6 , offset: const Offset ( 0 , 3 ) ) ,
] ,
) ,
child: Column (
crossAxisAlignment: CrossAxisAlignment . start ,
mainAxisSize: MainAxisSize . min ,
children: [
ClipRRect (
borderRadius: const BorderRadius . vertical ( top: Radius . circular ( cardRadius ) ) ,
child: img ! = null
? Image . network ( img , width: width , height: imageHeight , fit: BoxFit . cover , errorBuilder: ( _ , __ , ___ ) = > Container ( height: imageHeight , color: theme . dividerColor ) )
: Container ( height: imageHeight , width: width , color: theme . dividerColor ) ,
) ,
Padding (
padding: const EdgeInsets . fromLTRB ( horizontalPadding , verticalPadding , horizontalPadding , verticalPadding ) ,
child: Column (
crossAxisAlignment: CrossAxisAlignment . start ,
mainAxisSize: MainAxisSize . min ,
children: [
Text (
e . title ? ? e . name ? ? ' ' ,
maxLines: 2 ,
overflow: TextOverflow . ellipsis ,
style: theme . textTheme . bodyLarge ? . copyWith ( fontWeight: FontWeight . w700 , fontSize: 14 ) ,
) ,
const SizedBox ( height: 8 ) ,
Row (
children: [
Container (
width: 18 ,
height: 18 ,
decoration: BoxDecoration ( color: theme . colorScheme . primary . withOpacity ( 0.12 ) , borderRadius: BorderRadius . circular ( 4 ) ) ,
child: Icon ( Icons . calendar_today , size: 12 , color: theme . colorScheme . primary ) ,
) ,
const SizedBox ( width: 8 ) ,
Flexible ( child: Text ( dateLabel , maxLines: 1 , overflow: TextOverflow . ellipsis , style: theme . textTheme . bodySmall ? . copyWith ( color: theme . hintColor , fontSize: 13 ) ) ) ,
const Padding (
padding: EdgeInsets . symmetric ( horizontal: 8.0 ) ,
child: Text ( ' • ' , style: TextStyle ( color: Colors . black26 ) ) ,
) ,
Container (
width: 18 ,
height: 18 ,
decoration: BoxDecoration ( color: theme . colorScheme . primary . withOpacity ( 0.12 ) , borderRadius: BorderRadius . circular ( 4 ) ) ,
child: Icon ( Icons . location_on , size: 12 , color: theme . colorScheme . primary ) ,
) ,
const SizedBox ( width: 8 ) ,
Expanded ( child: Text ( e . place ? ? ' ' , maxLines: 1 , overflow: TextOverflow . ellipsis , style: theme . textTheme . bodySmall ? . copyWith ( color: theme . hintColor , fontSize: 13 ) ) ) ,
] ,
) ,
] ,
) ,
) ,
] ,
) ,
) ,
) ;
}
// ------------------------ Page routing ------------------------
Widget _getCurrentPage ( ) {
switch ( selectedMenu ) {
case 1 :
return const CalendarScreen ( ) ;
case 2 :
return const ProfileScreen ( ) ;
case 3 :
return BookingScreen ( onBook: ( ) { } , image: ' ' ) ;
case 4 :
release: bump version to 1.4(p) (versionCode 14)
- Update versionCode 12 → 14, versionName 1.3(p) → 1.4(p)
- Update pubspec.yaml version to 1.4.0+14
- Add CHANGELOG.md with full version history
- Update README.md: version badge + changelog section
- Desktop Contribute Dashboard rebuilt to match web version
- Contributor Dashboard title, 3-tab nav (Contribute/Leaderboard/Achievements)
- Two-column submit form, tier milestone progress bar
- Desktop leaderboard with podium, filters, rank table (green points)
- Desktop achievements 3-column badge grid
- Inline Reward Shop with RP balance
- Gamification feature module (EP, RP, leaderboard, achievements, shop)
- Profile screen redesigned to match web app layout with animations
- Home screen bottom sheet date filter chips
- Updated API endpoints, login/event detail screens, theme colors
- Added Gilroy font suite, responsive layout improvements
2026-03-18 11:10:56 +05:30
return ChangeNotifierProvider (
create: ( _ ) = > GamificationProvider ( ) ,
child: const ContributeScreen ( ) ,
2026-01-31 15:23:18 +05:30
) ;
case 5 :
return const SettingsScreen ( ) ;
default :
return _homeContent ( ) ;
}
}
// ------------------------ Build ------------------------
@ override
Widget build ( BuildContext context ) {
final theme = Theme . of ( context ) ;
return Scaffold (
backgroundColor: theme . scaffoldBackgroundColor ,
body: Row (
children: [
_buildLeftPanel ( ) ,
Expanded (
child: Column (
children: [
// Show top bar ONLY when Home is active
if ( selectedMenu = = 0 ) _buildTopBar ( ) ,
// Page content under the top bar (or directly if top bar hidden)
Expanded ( child: _getCurrentPage ( ) ) ,
] ,
) ,
) ,
] ,
) ,
) ;
}
}