Files
Eventify-frontend/lib/screens/home_screen.dart

2046 lines
76 KiB
Dart
Raw Normal View History

2026-01-31 15:23:18 +05:30
// lib/screens/home_screen.dart
import 'dart:async';
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:cached_network_image/cached_network_image.dart';
2026-01-31 15:23:18 +05:30
import '../features/events/models/event_models.dart';
import '../features/events/services/events_service.dart';
import 'calendar_screen.dart';
import 'profile_screen.dart';
import 'contribute_screen.dart';
import 'learn_more_screen.dart';
import 'search_screen.dart';
import '../core/app_decoration.dart';
2026-03-11 20:13:13 +05:30
import 'package:flutter_svg/flutter_svg.dart';
import 'package:provider/provider.dart';
import '../features/gamification/providers/gamification_provider.dart';
2026-01-31 15:23:18 +05:30
class HomeScreen extends StatefulWidget {
const HomeScreen({Key? key}) : super(key: key);
@override
_HomeScreenState createState() => _HomeScreenState();
}
/// Main screen that hosts 4 tabs in an IndexedStack (Home, Calendar, Contribute, Profile).
class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateMixin {
int _selectedIndex = 0;
String _username = '';
String _location = '';
String _pincode = 'all';
final EventsService _eventsService = EventsService();
// backend-driven
List<EventModel> _events = [];
List<EventTypeModel> _types = [];
int _selectedTypeId = -1; // -1 == All
bool _loading = true;
// Hero carousel
final PageController _heroPageController = PageController();
int _heroCurrentPage = 0;
Timer? _autoScrollTimer;
@override
void initState() {
super.initState();
_loadUserDataAndEvents();
_startAutoScroll();
}
@override
void dispose() {
_autoScrollTimer?.cancel();
_heroPageController.dispose();
super.dispose();
}
void _startAutoScroll() {
_autoScrollTimer?.cancel();
_autoScrollTimer = Timer.periodic(const Duration(seconds: 3), (timer) {
if (_heroEvents.isEmpty) return;
final nextPage = (_heroCurrentPage + 1) % _heroEvents.length;
if (_heroPageController.hasClients) {
_heroPageController.animateToPage(
nextPage,
duration: const Duration(milliseconds: 500),
curve: Curves.easeInOut,
);
}
});
}
Future<void> _loadUserDataAndEvents() async {
setState(() => _loading = true);
final prefs = await SharedPreferences.getInstance();
_username = prefs.getString('display_name') ?? prefs.getString('username') ?? '';
_location = prefs.getString('location') ?? 'Whitefield, Bengaluru';
_pincode = prefs.getString('pincode') ?? 'all';
try {
// Fetch types and events in parallel for faster loading
final results = await Future.wait([
_events_service_getEventTypesSafe(),
_events_service_getEventsSafe(_pincode),
]);
final types = results[0] as List<EventTypeModel>;
final events = results[1] as List<EventModel>;
2026-01-31 15:23:18 +05:30
if (mounted) {
setState(() {
_types = types;
_events = events;
_selectedTypeId = -1;
_cachedFilteredEvents = null; // invalidate cache
2026-01-31 15:23:18 +05:30
});
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(e.toString())));
}
} finally {
if (mounted) setState(() => _loading = false);
}
}
Future<List<EventTypeModel>> _events_service_getEventTypesSafe() async {
try {
return await _eventsService.getEventTypes();
} catch (_) {
return <EventTypeModel>[];
}
}
Future<List<EventModel>> _events_service_getEventsSafe(String pincode) async {
try {
return await _eventsService.getEventsByPincode(pincode);
} catch (_) {
return <EventModel>[];
}
}
Future<void> _refresh() async {
await _loadUserDataAndEvents();
}
void _bookEventAtIndex(int index) {
if (index >= 0 && index < _events.length) {
setState(() => _events.removeAt(index));
}
}
Widget _categoryChip({
required String label,
required bool selected,
required VoidCallback onTap,
2026-03-11 20:13:13 +05:30
String? imageUrl,
2026-01-31 15:23:18 +05:30
IconData? icon,
}) {
final theme = Theme.of(context);
2026-03-11 20:13:13 +05:30
return GestureDetector(
2026-01-31 15:23:18 +05:30
onTap: onTap,
child: Container(
2026-03-11 20:13:13 +05:30
width: 110,
2026-01-31 15:23:18 +05:30
decoration: BoxDecoration(
2026-03-11 20:13:13 +05:30
color: selected ? theme.colorScheme.primary : Colors.white,
borderRadius: BorderRadius.circular(18),
boxShadow: [
BoxShadow(
color: selected
? theme.colorScheme.primary.withOpacity(0.35)
: Colors.black.withOpacity(0.06),
blurRadius: selected ? 12 : 8,
offset: const Offset(0, 4),
),
],
2026-01-31 15:23:18 +05:30
),
2026-03-11 20:13:13 +05:30
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
2026-01-31 15:23:18 +05:30
children: [
2026-03-11 20:13:13 +05:30
// Image / Icon area
SizedBox(
height: 56,
width: 56,
child: imageUrl != null && imageUrl.isNotEmpty
? ClipRRect(
borderRadius: BorderRadius.circular(12),
child: CachedNetworkImage(
imageUrl: imageUrl,
2026-03-11 20:13:13 +05:30
fit: BoxFit.contain,
placeholder: (_, __) => Icon(
icon ?? Icons.category,
size: 36,
color: selected ? Colors.white.withOpacity(0.5) : theme.colorScheme.primary.withOpacity(0.3),
),
errorWidget: (_, __, ___) => Icon(
2026-03-11 20:13:13 +05:30
icon ?? Icons.category,
size: 36,
color: selected ? Colors.white : theme.colorScheme.primary,
),
),
)
: Icon(
icon ?? Icons.category,
size: 36,
color: selected ? Colors.white : theme.colorScheme.primary,
),
),
const SizedBox(height: 10),
// Label
Padding(
padding: const EdgeInsets.symmetric(horizontal: 6),
child: Text(
label,
maxLines: 1,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w700,
color: selected
? Colors.white
: theme.textTheme.bodyLarge?.color ?? Colors.black87,
),
2026-01-31 15:23:18 +05:30
),
),
],
),
),
);
}
Future<void> _openLocationSearch() async {
final selected = await Navigator.of(context).push(PageRouteBuilder(
opaque: false,
pageBuilder: (context, animation, secondaryAnimation) => const SearchScreen(),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return FadeTransition(opacity: animation, child: child);
},
transitionDuration: const Duration(milliseconds: 220),
));
if (selected != null && selected is String) {
final prefs = await SharedPreferences.getInstance();
await prefs.setString('location', selected);
setState(() {
_location = selected;
});
await _refresh();
}
}
void _openEventSearch() {
final theme = Theme.of(context);
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (ctx) {
return DraggableScrollableSheet(
expand: false,
initialChildSize: 0.6,
minChildSize: 0.3,
maxChildSize: 0.95,
builder: (context, scrollController) {
String query = '';
List<EventModel> results = List.from(_events);
return StatefulBuilder(builder: (context, setModalState) {
void _onQueryChanged(String v) {
query = v.trim().toLowerCase();
final r = _events.where((e) {
final title = (e.title ?? e.name ?? '').toLowerCase();
return title.contains(query);
}).toList();
setModalState(() {
results = r;
});
}
return Container(
decoration: BoxDecoration(
color: theme.cardColor,
borderRadius: const BorderRadius.vertical(top: Radius.circular(20)),
),
child: SafeArea(
top: false,
child: SingleChildScrollView(
controller: scrollController,
padding: const EdgeInsets.fromLTRB(16, 12, 16, 24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Center(
child: Container(
width: 48,
height: 6,
decoration: BoxDecoration(color: theme.dividerColor, borderRadius: BorderRadius.circular(6)),
),
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: Container(
height: 48,
padding: const EdgeInsets.symmetric(horizontal: 12),
decoration: BoxDecoration(color: theme.cardColor, borderRadius: BorderRadius.circular(12), border: Border.all(color: theme.dividerColor)),
child: Row(
children: [
Icon(Icons.search, color: theme.hintColor),
const SizedBox(width: 8),
Expanded(
child: TextField(
style: theme.textTheme.bodyLarge,
decoration: InputDecoration(
hintText: 'Search events by name',
hintStyle: theme.textTheme.bodyMedium?.copyWith(color: theme.hintColor),
border: InputBorder.none,
),
autofocus: true,
onChanged: _onQueryChanged,
textInputAction: TextInputAction.search,
onSubmitted: (v) => _onQueryChanged(v),
),
)
],
),
),
),
const SizedBox(width: 8),
IconButton(
icon: Icon(Icons.close, color: theme.iconTheme.color),
onPressed: () => Navigator.of(context).pop(),
)
],
),
const SizedBox(height: 12),
if (_loading)
Center(child: CircularProgressIndicator(color: theme.colorScheme.primary))
else if (results.isEmpty)
Padding(
padding: const EdgeInsets.symmetric(vertical: 24),
child: Center(child: Text(query.isEmpty ? 'Type to search events' : 'No events found', style: theme.textTheme.bodyMedium?.copyWith(color: theme.hintColor))),
)
else
ListView.separated(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (ctx, idx) {
final ev = results[idx];
final img = (ev.thumbImg != null && ev.thumbImg!.isNotEmpty) ? ev.thumbImg! : (ev.images.isNotEmpty ? ev.images.first.image : null);
final title = ev.title ?? ev.name ?? '';
final subtitle = ev.startDate ?? '';
return ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 0, vertical: 8),
leading: img != null && img.isNotEmpty
? ClipRRect(borderRadius: BorderRadius.circular(8), child: Image.network(img, width: 56, height: 56, fit: BoxFit.cover))
: Container(width: 56, height: 56, decoration: BoxDecoration(color: theme.dividerColor, borderRadius: BorderRadius.circular(8)), child: Icon(Icons.event, color: theme.hintColor)),
title: Text(title, maxLines: 1, overflow: TextOverflow.ellipsis, style: theme.textTheme.bodyLarge),
subtitle: Text(subtitle, maxLines: 1, overflow: TextOverflow.ellipsis, style: theme.textTheme.bodyMedium?.copyWith(color: theme.hintColor)),
onTap: () {
Navigator.of(context).pop();
if (ev.id != null) {
Navigator.of(context).push(MaterialPageRoute(builder: (_) => LearnMoreScreen(eventId: ev.id)));
}
},
);
},
separatorBuilder: (_, __) => Divider(color: theme.dividerColor),
itemCount: results.length,
),
],
),
),
),
);
});
},
);
},
);
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Scaffold(
backgroundColor: theme.scaffoldBackgroundColor,
body: Stack(
children: [
// IndexedStack keeps each tab alive and preserves state.
IndexedStack(
index: _selectedIndex,
children: [
_buildHomeContent(), // index 0
const CalendarScreen(), // index 1
ChangeNotifierProvider(
create: (_) => GamificationProvider(),
child: const ContributeScreen(),
), // index 2 (full page, scrollable)
2026-01-31 15:23:18 +05:30
const ProfileScreen(), // index 3
],
),
// Floating bottom navigation (always visible)
Positioned(
left: 16,
right: 16,
bottom: 16,
child: _buildFloatingBottomNav(),
),
],
),
);
}
Widget _buildFloatingBottomNav() {
const activeColor = Color(0xFF2563EB);
const inactiveColor = Color(0xFF9CA3AF);
2026-01-31 15:23:18 +05:30
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
2026-01-31 15:23:18 +05:30
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(color: Colors.black.withOpacity(0.08), blurRadius: 16, offset: const Offset(0, -2)),
],
2026-01-31 15:23:18 +05:30
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_bottomNavItem(0, Icons.home_outlined, Icons.home, 'Home'),
_bottomNavItem(1, Icons.calendar_today_outlined, Icons.calendar_today, 'Calendar'),
_bottomNavItem(2, Icons.front_hand_outlined, Icons.front_hand, 'Contribute'),
_bottomNavItem(3, Icons.person_outline, Icons.person, 'Profile'),
2026-01-31 15:23:18 +05:30
],
),
);
}
Widget _bottomNavItem(int index, IconData outlinedIcon, IconData filledIcon, String label) {
const activeColor = Color(0xFF2563EB);
const inactiveColor = Color(0xFF9CA3AF);
final active = _selectedIndex == index;
2026-03-11 20:13:13 +05:30
2026-01-31 15:23:18 +05:30
return GestureDetector(
onTap: () => setState(() => _selectedIndex = index),
behavior: HitTestBehavior.opaque,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
active ? filledIcon : outlinedIcon,
color: active ? activeColor : inactiveColor,
size: 24,
2026-03-11 20:13:13 +05:30
),
const SizedBox(height: 4),
Text(
label,
style: TextStyle(
color: active ? activeColor : inactiveColor,
fontSize: 11,
fontWeight: active ? FontWeight.w600 : FontWeight.w400,
),
2026-03-11 20:13:13 +05:30
),
],
),
2026-03-11 20:13:13 +05:30
),
2026-01-31 15:23:18 +05:30
);
}
2026-03-11 20:13:13 +05:30
2026-01-31 15:23:18 +05:30
// Get hero events (first 4 events for the carousel)
List<EventModel> get _heroEvents => _events.take(4).toList();
// Date filter state
String _selectedDateFilter = '';
DateTime? _selectedCustomDate;
// Cached filtered events to avoid repeated DateTime.parse() calls
List<EventModel>? _cachedFilteredEvents;
String _cachedFilterKey = '';
/// Returns the subset of [_events] that match the active date-filter chip.
/// Uses caching to avoid re-parsing dates on every access.
List<EventModel> get _filteredEvents {
if (_selectedDateFilter.isEmpty) return _events;
// Build a cache key from filter state
final cacheKey = '$_selectedDateFilter|$_selectedCustomDate|${_events.length}';
if (_cachedFilteredEvents != null && _cachedFilterKey == cacheKey) {
return _cachedFilteredEvents!;
}
final now = DateTime.now();
final today = DateTime(now.year, now.month, now.day);
DateTime filterStart;
DateTime filterEnd;
2026-01-31 15:23:18 +05:30
switch (_selectedDateFilter) {
case 'Today':
filterStart = today;
filterEnd = today;
break;
case 'Tomorrow':
final tomorrow = today.add(const Duration(days: 1));
filterStart = tomorrow;
filterEnd = tomorrow;
break;
case 'This week':
// MondaySunday of the current week
final weekday = today.weekday; // 1=Mon
filterStart = today.subtract(Duration(days: weekday - 1));
filterEnd = filterStart.add(const Duration(days: 6));
break;
case 'Date':
if (_selectedCustomDate == null) return _events;
filterStart = _selectedCustomDate!;
filterEnd = _selectedCustomDate!;
break;
default:
return _events;
}
_cachedFilteredEvents = _events.where((e) {
try {
final eStart = DateTime.parse(e.startDate);
final eEnd = DateTime.parse(e.endDate);
// Event overlaps with filter range
return !eEnd.isBefore(filterStart) && !eStart.isAfter(filterEnd);
} catch (_) {
return false;
2026-01-31 15:23:18 +05:30
}
}).toList();
_cachedFilterKey = cacheKey;
return _cachedFilteredEvents!;
}
Future<void> _onDateChipTap(String label) async {
if (label == 'Date') {
// Open custom calendar dialog
final picked = await _showCalendarDialog();
if (picked != null) {
setState(() {
_selectedCustomDate = picked;
_selectedDateFilter = 'Date';
_cachedFilteredEvents = null; // invalidate cache
});
_showFilteredEventsSheet(
'${picked.day.toString().padLeft(2, '0')} ${_monthName(picked.month)} ${picked.year}',
);
} else if (_selectedDateFilter == 'Date') {
setState(() {
_selectedDateFilter = '';
_selectedCustomDate = null;
_cachedFilteredEvents = null;
});
}
} else {
setState(() {
_selectedDateFilter = label;
_selectedCustomDate = null;
_cachedFilteredEvents = null; // invalidate cache
});
_showFilteredEventsSheet(label);
}
}
String _monthName(int m) {
const months = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
return months[m - 1];
}
/// Shows a bottom sheet with events matching the current filter chip.
void _showFilteredEventsSheet(String title) {
final theme = Theme.of(context);
final filtered = _filteredEvents;
final count = filtered.length;
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
barrierColor: Colors.black.withValues(alpha: 0.5),
builder: (ctx) {
return DraggableScrollableSheet(
expand: false,
initialChildSize: 0.55,
minChildSize: 0.3,
maxChildSize: 0.85,
builder: (context, scrollController) {
return Container(
decoration: const BoxDecoration(
color: Color(0xFFEAEFFE), // lavender sheet bg matching web
borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
),
child: Column(
children: [
// Drag handle
Padding(
padding: const EdgeInsets.only(top: 12, bottom: 8),
child: Center(
child: Container(
width: 40,
height: 5,
decoration: BoxDecoration(
color: Colors.grey.shade400,
borderRadius: BorderRadius.circular(3),
),
),
),
),
// Header row: title + close button
Padding(
padding: const EdgeInsets.fromLTRB(20, 4, 12, 12),
child: Row(
children: [
Expanded(
child: Text(
'$title ($count)',
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.w700,
color: Color(0xFF1A1A1A),
),
),
),
GestureDetector(
onTap: () {
Navigator.of(context).pop();
setState(() {
_selectedDateFilter = '';
_selectedCustomDate = null;
});
},
child: Container(
width: 32,
height: 32,
decoration: BoxDecoration(
color: Colors.grey.shade300,
shape: BoxShape.circle,
),
child: const Icon(Icons.close, size: 18, color: Color(0xFF1A1A1A)),
),
),
],
),
),
// Events list
Expanded(
child: filtered.isEmpty
? Padding(
padding: const EdgeInsets.all(24),
child: Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(vertical: 32, horizontal: 16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
),
child: const Text(
'\u{1F3D7}\u{FE0F} No events scheduled for this period',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.w500,
color: Color(0xFF6B7280),
),
),
),
)
: ListView.builder(
controller: scrollController,
padding: const EdgeInsets.fromLTRB(16, 0, 16, 24),
itemCount: filtered.length,
itemBuilder: (ctx, idx) {
final ev = filtered[idx];
return _buildSheetEventCard(ev, theme);
},
),
),
],
),
);
},
);
},
).whenComplete(() {
// Clear filter when sheet is dismissed
setState(() {
_selectedDateFilter = '';
_selectedCustomDate = null;
});
});
}
/// Builds an event card for the filter bottom sheet, matching web design.
Widget _buildSheetEventCard(EventModel ev, ThemeData theme) {
final title = ev.title ?? ev.name ?? '';
final dateLabel = ev.startDate ?? '';
final location = ev.place ?? 'Location';
final imageUrl = (ev.thumbImg != null && ev.thumbImg!.isNotEmpty)
? ev.thumbImg!
: (ev.images.isNotEmpty ? ev.images.first.image : null);
Widget imageWidget;
if (imageUrl != null && imageUrl.isNotEmpty && imageUrl.startsWith('http')) {
imageWidget = ClipRRect(
borderRadius: BorderRadius.circular(12),
child: CachedNetworkImage(
imageUrl: imageUrl,
width: 80,
height: 80,
fit: BoxFit.cover,
placeholder: (_, __) => Container(
width: 80, height: 80,
decoration: BoxDecoration(color: Colors.grey.shade200, borderRadius: BorderRadius.circular(12)),
child: const Center(child: SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2))),
),
errorWidget: (_, __, ___) => Container(
width: 80, height: 80,
decoration: BoxDecoration(color: Colors.grey.shade200, borderRadius: BorderRadius.circular(12)),
child: Icon(Icons.image, color: Colors.grey.shade400),
),
),
);
} else {
imageWidget = Container(
width: 80, height: 80,
decoration: BoxDecoration(color: Colors.grey.shade200, borderRadius: BorderRadius.circular(12)),
child: Icon(Icons.image, color: Colors.grey.shade400),
);
2026-01-31 15:23:18 +05:30
}
return GestureDetector(
onTap: () {
Navigator.of(context).pop();
if (ev.id != null) {
Navigator.of(context).push(MaterialPageRoute(builder: (_) => LearnMoreScreen(eventId: ev.id)));
}
},
child: Container(
margin: const EdgeInsets.only(bottom: 12),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.04),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: Row(
children: [
imageWidget,
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.w700,
color: Color(0xFF1A1A1A),
),
),
const SizedBox(height: 4),
Row(
children: [
Icon(Icons.calendar_today, size: 13, color: Colors.grey.shade500),
const SizedBox(width: 4),
Expanded(
child: Text(
dateLabel,
style: TextStyle(fontSize: 13, color: Colors.grey.shade500),
overflow: TextOverflow.ellipsis,
),
),
],
),
const SizedBox(height: 2),
Row(
children: [
Icon(Icons.location_on_outlined, size: 13, color: Colors.grey.shade500),
const SizedBox(width: 4),
Expanded(
child: Text(
location,
style: TextStyle(fontSize: 13, color: Colors.grey.shade500),
overflow: TextOverflow.ellipsis,
),
),
],
),
const SizedBox(height: 4),
Text(
'Free',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: theme.colorScheme.primary,
),
),
],
),
),
],
),
),
);
}
2026-01-31 15:23:18 +05:30
/// Collect all event dates (start + end range) to show dots on the calendar.
Set<DateTime> get _eventDates {
final dates = <DateTime>{};
for (final e in _events) {
try {
final start = DateTime.parse(e.startDate);
final end = DateTime.parse(e.endDate);
var d = DateTime(start.year, start.month, start.day);
final last = DateTime(end.year, end.month, end.day);
while (!d.isAfter(last)) {
dates.add(d);
d = d.add(const Duration(days: 1));
}
} catch (_) {}
}
return dates;
}
2026-01-31 15:23:18 +05:30
/// Show a custom calendar dialog matching the app design.
Future<DateTime?> _showCalendarDialog() {
DateTime viewMonth = _selectedCustomDate ?? DateTime.now();
DateTime? selected = _selectedCustomDate;
final eventDates = _eventDates;
return showDialog<DateTime>(
context: context,
barrierColor: Colors.black54,
builder: (ctx) {
return StatefulBuilder(builder: (ctx, setDialogState) {
final now = DateTime.now();
final today = DateTime(now.year, now.month, now.day);
2026-01-31 15:23:18 +05:30
// Calendar calculations
final firstDayOfMonth = DateTime(viewMonth.year, viewMonth.month, 1);
final daysInMonth = DateTime(viewMonth.year, viewMonth.month + 1, 0).day;
final startWeekday = firstDayOfMonth.weekday; // 1=Mon
2026-01-31 15:23:18 +05:30
const months = ['January','February','March','April','May','June','July','August','September','October','November','December'];
const dayHeaders = ['M', 'T', 'W', 'T', 'F', 'S', 'S'];
return Center(
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 28),
padding: const EdgeInsets.fromLTRB(20, 24, 20, 20),
2026-01-31 15:23:18 +05:30
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(24),
boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.15), blurRadius: 20, offset: const Offset(0, 8))],
2026-01-31 15:23:18 +05:30
),
child: Material(
color: Colors.transparent,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Month navigation
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
_calendarNavButton(Icons.chevron_left, () {
setDialogState(() {
viewMonth = DateTime(viewMonth.year, viewMonth.month - 1, 1);
});
}),
Text(
'${months[viewMonth.month - 1]} ${viewMonth.year}',
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Color(0xFF1A1A2E)),
),
_calendarNavButton(Icons.chevron_right, () {
setDialogState(() {
viewMonth = DateTime(viewMonth.year, viewMonth.month + 1, 1);
});
}),
],
),
const SizedBox(height: 20),
// Day of week headers
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: dayHeaders.map((d) => SizedBox(
width: 36,
child: Center(child: Text(d, style: TextStyle(color: Colors.grey[500], fontWeight: FontWeight.w600, fontSize: 13))),
)).toList(),
),
const SizedBox(height: 12),
// Calendar grid
...List.generate(6, (weekRow) {
return Padding(
padding: const EdgeInsets.only(bottom: 4),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: List.generate(7, (weekCol) {
final cellIndex = weekRow * 7 + weekCol;
final dayNum = cellIndex - (startWeekday - 1) + 1;
if (dayNum < 1 || dayNum > daysInMonth) {
return const SizedBox(width: 36, height: 44);
}
final cellDate = DateTime(viewMonth.year, viewMonth.month, dayNum);
final isToday = cellDate == today;
final isSelected = selected != null &&
cellDate.year == selected!.year &&
cellDate.month == selected!.month &&
cellDate.day == selected!.day;
final hasEvent = eventDates.contains(cellDate);
return GestureDetector(
onTap: () {
setDialogState(() => selected = cellDate);
},
child: SizedBox(
width: 36,
height: 44,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
width: 34,
height: 34,
decoration: BoxDecoration(
color: isSelected
? const Color(0xFF2563EB)
: Colors.transparent,
shape: BoxShape.circle,
border: isToday && !isSelected
? Border.all(color: const Color(0xFF2563EB), width: 1.5)
: null,
),
child: Center(
child: Text(
'$dayNum',
style: TextStyle(
fontSize: 15,
fontWeight: (isToday || isSelected) ? FontWeight.w700 : FontWeight.w500,
color: isSelected
? Colors.white
: isToday
? const Color(0xFF2563EB)
: const Color(0xFF374151),
),
),
),
),
// Event dot
if (hasEvent)
Container(
width: 5,
height: 5,
decoration: BoxDecoration(
color: isSelected ? Colors.white : const Color(0xFFEF4444),
shape: BoxShape.circle,
),
)
else
const SizedBox(height: 5),
],
),
),
);
}),
),
);
}),
const SizedBox(height: 12),
// Done button
SizedBox(
width: 160,
height: 48,
child: ElevatedButton(
onPressed: () => Navigator.of(ctx).pop(selected),
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF2563EB),
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)),
elevation: 0,
),
child: const Text('Done', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600)),
),
),
],
),
),
),
);
});
},
2026-01-31 15:23:18 +05:30
);
}
Widget _calendarNavButton(IconData icon, VoidCallback onTap) {
return InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(20),
child: Container(
width: 40,
height: 40,
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(color: Colors.grey[300]!),
),
child: Icon(icon, color: const Color(0xFF374151), size: 22),
),
);
}
2026-01-31 15:23:18 +05:30
Widget _buildHomeContent() {
return Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Color(0xFF1A0A2E), // deep purple
Color(0xFF16213E), // dark navy
Color(0xFF0A0A0A), // near black
],
stops: [0.0, 0.4, 0.8],
),
),
child: CustomScrollView(
slivers: [
// Hero section (dark bg)
SliverToBoxAdapter(child: _buildHeroSection()),
// White bottom section
SliverToBoxAdapter(
child: Container(
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.vertical(top: Radius.circular(30)),
),
child: _buildWhiteSection(),
),
),
],
),
);
}
2026-01-31 15:23:18 +05:30
Widget _buildHeroSection() {
2026-01-31 15:23:18 +05:30
return SafeArea(
bottom: false,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Top bar: location pill + search button
2026-01-31 15:23:18 +05:30
Padding(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
GestureDetector(
onTap: _openLocationSearch,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.15),
2026-01-31 15:23:18 +05:30
borderRadius: BorderRadius.circular(25),
border: Border.all(color: Colors.white.withOpacity(0.2)),
2026-01-31 15:23:18 +05:30
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.location_on_outlined, color: Colors.white, size: 18),
const SizedBox(width: 6),
Text(
_location.length > 20 ? '${_location.substring(0, 20)}...' : _location,
style: const TextStyle(color: Colors.white, fontSize: 14, fontWeight: FontWeight.w500),
),
const SizedBox(width: 4),
const Icon(Icons.keyboard_arrow_down, color: Colors.white, size: 18),
],
),
),
),
GestureDetector(
onTap: _openEventSearch,
child: Container(
width: 48,
height: 48,
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.15),
2026-01-31 15:23:18 +05:30
shape: BoxShape.circle,
border: Border.all(color: Colors.white.withOpacity(0.2)),
2026-01-31 15:23:18 +05:30
),
child: const Icon(Icons.search, color: Colors.white, size: 24),
),
),
],
),
),
const SizedBox(height: 24),
2026-01-31 15:23:18 +05:30
// Featured carousel
2026-01-31 15:23:18 +05:30
_heroEvents.isEmpty
? SizedBox(
height: 280,
2026-01-31 15:23:18 +05:30
child: Center(
child: _loading
? const CircularProgressIndicator(color: Colors.white)
: const Text('No events available', style: TextStyle(color: Colors.white70)),
2026-01-31 15:23:18 +05:30
),
)
: Column(
children: [
SizedBox(
2026-03-11 20:13:13 +05:30
height: 300,
2026-01-31 15:23:18 +05:30
child: PageView.builder(
controller: _heroPageController,
onPageChanged: (page) => setState(() => _heroCurrentPage = page),
2026-01-31 15:23:18 +05:30
itemCount: _heroEvents.length,
itemBuilder: (context, index) => _buildHeroEventImage(_heroEvents[index]),
2026-01-31 15:23:18 +05:30
),
),
const SizedBox(height: 16),
// Pagination dots
_buildCarouselDots(),
2026-01-31 15:23:18 +05:30
],
),
const SizedBox(height: 24),
2026-01-31 15:23:18 +05:30
],
),
);
}
Widget _buildCarouselDots() {
return SizedBox(
height: 12,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: List.generate(
_heroEvents.isEmpty ? 5 : _heroEvents.length,
(i) {
final isActive = i == _heroCurrentPage;
return GestureDetector(
onTap: () {
if (_heroPageController.hasClients) {
_heroPageController.animateToPage(i,
duration: const Duration(milliseconds: 300), curve: Curves.easeInOut);
}
},
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 4),
width: isActive ? 24 : 8,
height: 8,
decoration: BoxDecoration(
color: isActive ? Colors.white : Colors.white.withOpacity(0.4),
borderRadius: BorderRadius.circular(4),
),
),
);
},
),
),
);
}
2026-03-11 20:13:13 +05:30
/// Build a hero image card with the image only (rounded),
/// and the title text placed below the image.
2026-01-31 15:23:18 +05:30
Widget _buildHeroEventImage(EventModel event) {
String? img;
if (event.thumbImg != null && event.thumbImg!.isNotEmpty) {
img = event.thumbImg;
} else if (event.images.isNotEmpty && event.images.first.image.isNotEmpty) {
img = event.images.first.image;
}
final radius = 24.0;
return GestureDetector(
onTap: () {
if (event.id != null) {
Navigator.push(context, MaterialPageRoute(builder: (_) => LearnMoreScreen(eventId: event.id)));
}
},
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
2026-03-11 20:13:13 +05:30
child: Column(
children: [
// Image only (no text overlay)
Expanded(
child: ClipRRect(
borderRadius: BorderRadius.circular(radius),
child: SizedBox(
width: double.infinity,
child: img != null && img.isNotEmpty
? Image.network(
img,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) =>
Container(decoration: AppDecoration.blueGradientRounded(radius)),
)
: Container(
decoration: AppDecoration.blueGradientRounded(radius),
),
2026-01-31 15:23:18 +05:30
),
),
2026-03-11 20:13:13 +05:30
),
2026-01-31 15:23:18 +05:30
2026-03-11 20:13:13 +05:30
// Title text outside the image
const SizedBox(height: 12),
Text(
event.title ?? event.name ?? '',
maxLines: 2,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center,
style: const TextStyle(
color: Colors.white,
fontSize: 22,
fontWeight: FontWeight.bold,
height: 1.2,
shadows: [
Shadow(color: Colors.black38, blurRadius: 6, offset: Offset(0, 2)),
],
2026-01-31 15:23:18 +05:30
),
2026-03-11 20:13:13 +05:30
),
],
2026-01-31 15:23:18 +05:30
),
),
);
}
Widget _buildWhiteSection() {
return Padding(
padding: const EdgeInsets.fromLTRB(16, 24, 16, 0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Search bar
GestureDetector(
onTap: _openEventSearch,
child: Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
decoration: BoxDecoration(
color: const Color(0xFFF3F4F6),
borderRadius: BorderRadius.circular(28),
2026-01-31 15:23:18 +05:30
),
child: const Text(
'Search events, artists or attractions',
style: TextStyle(color: Color(0xFF9CA3AF), fontSize: 14),
2026-01-31 15:23:18 +05:30
),
),
2026-01-31 15:23:18 +05:30
),
const SizedBox(height: 16),
2026-01-31 15:23:18 +05:30
// Date filter chips
SizedBox(
height: 40,
child: ListView(
scrollDirection: Axis.horizontal,
children: [
_dateChip(label: 'Date', icon: Icons.calendar_today, hasDropdown: true),
const SizedBox(width: 8),
_dateChip(label: 'Today'),
const SizedBox(width: 8),
_dateChip(label: 'Tomorrow'),
const SizedBox(width: 8),
_dateChip(label: 'This week'),
],
),
),
const SizedBox(height: 24),
// Top Events
const Text(
'Top Events',
style: TextStyle(
color: Color(0xFF111827),
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
SizedBox(
height: 200,
child: _filteredEvents.isEmpty && _loading
? const Center(child: CircularProgressIndicator())
: _filteredEvents.isEmpty
? Center(child: Text(
_selectedDateFilter.isNotEmpty ? 'No events for "$_selectedDateFilter"' : 'No events found',
style: const TextStyle(color: Color(0xFF9CA3AF)),
))
: ListView.separated(
scrollDirection: Axis.horizontal,
itemCount: _filteredEvents.length,
separatorBuilder: (_, __) => const SizedBox(width: 12),
itemBuilder: (context, index) => _buildTopEventCard(_filteredEvents[index]),
),
),
const SizedBox(height: 24),
// Events Around You
Row(
2026-01-31 15:23:18 +05:30
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
2026-01-31 15:23:18 +05:30
'Events Around You',
style: TextStyle(
color: Color(0xFF111827),
fontSize: 20,
2026-01-31 15:23:18 +05:30
fontWeight: FontWeight.bold,
),
),
TextButton(
onPressed: () {},
child: const Text(
2026-01-31 15:23:18 +05:30
'View All',
style: TextStyle(color: Color(0xFF2563EB), fontWeight: FontWeight.w600),
2026-01-31 15:23:18 +05:30
),
),
],
),
const SizedBox(height: 12),
2026-01-31 15:23:18 +05:30
// Category chips (card-style)
SizedBox(
height: 140,
child: ListView(
scrollDirection: Axis.horizontal,
children: [
2026-01-31 15:23:18 +05:30
_categoryChip(
label: 'All Events',
icon: Icons.grid_view_rounded,
selected: _selectedTypeId == -1,
onTap: () => _onSelectType(-1),
2026-01-31 15:23:18 +05:30
),
2026-03-11 20:13:13 +05:30
const SizedBox(width: 12),
for (final t in _types) ...[
_categoryChip(
label: t.name,
imageUrl: t.iconUrl,
icon: _getIconForType(t.name),
selected: _selectedTypeId == t.id,
onTap: () => _onSelectType(t.id),
),
const SizedBox(width: 12),
],
2026-01-31 15:23:18 +05:30
],
),
2026-01-31 15:23:18 +05:30
),
const SizedBox(height: 16),
2026-01-31 15:23:18 +05:30
// Event sections by type
if (_selectedTypeId == -1) ...[
if (_loading)
const Padding(
padding: EdgeInsets.all(40),
child: Center(child: CircularProgressIndicator()),
)
else if (_filteredEvents.isEmpty && _selectedDateFilter.isNotEmpty)
Padding(
padding: const EdgeInsets.all(40),
child: Center(child: Text(
'No events for "$_selectedDateFilter"',
style: const TextStyle(color: Color(0xFF9CA3AF)),
)),
)
else
Column(
2026-03-11 20:13:13 +05:30
children: [
for (final t in _types)
if (_filteredEvents.where((e) => e.eventTypeId == t.id).isNotEmpty) ...[
2026-03-11 20:13:13 +05:30
_buildTypeSection(t),
const SizedBox(height: 18),
],
],
2026-01-31 15:23:18 +05:30
),
] else ...[
if (_loading)
const Padding(
padding: EdgeInsets.all(40),
child: Center(child: CircularProgressIndicator()),
)
else
Column(
children: _filteredEvents.map((e) => _buildFullWidthCard(e)).toList(),
2026-03-11 20:13:13 +05:30
),
],
// Bottom padding for nav bar
const SizedBox(height: 100),
2026-03-11 20:13:13 +05:30
],
),
);
}
Widget _dateChip({required String label, IconData? icon, bool hasDropdown = false}) {
final isSelected = _selectedDateFilter == label;
// For 'Date' chip, show the picked date instead of just "Date"
String displayLabel = label;
if (label == 'Date' && isSelected && _selectedCustomDate != null) {
final d = _selectedCustomDate!;
const months = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
displayLabel = '${d.day} ${months[d.month - 1]}';
}
return GestureDetector(
onTap: () => _onDateChipTap(label),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
decoration: BoxDecoration(
color: isSelected ? const Color(0xFF2563EB) : const Color(0xFFF3F4F6),
borderRadius: BorderRadius.circular(20),
border: Border.all(color: isSelected ? const Color(0xFF2563EB) : const Color(0xFFE5E7EB)),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (icon != null) ...[
Icon(icon, size: 16, color: isSelected ? Colors.white : const Color(0xFF374151)),
const SizedBox(width: 6),
],
Text(
displayLabel,
style: TextStyle(
color: isSelected ? Colors.white : const Color(0xFF374151),
fontSize: 13,
fontWeight: FontWeight.w500,
),
),
if (hasDropdown) ...[
const SizedBox(width: 4),
Icon(Icons.keyboard_arrow_down, size: 16, color: isSelected ? Colors.white : const Color(0xFF374151)),
],
],
),
),
);
}
Widget _buildTopEventCard(EventModel event) {
String? img;
if (event.thumbImg != null && event.thumbImg!.isNotEmpty) {
img = event.thumbImg;
} else if (event.images.isNotEmpty && event.images.first.image.isNotEmpty) {
img = event.images.first.image;
}
2026-01-31 15:23:18 +05:30
return GestureDetector(
onTap: () {
if (event.id != null) {
Navigator.push(context, MaterialPageRoute(builder: (_) => LearnMoreScreen(eventId: event.id)));
}
},
child: Container(
width: 150,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
),
clipBehavior: Clip.antiAlias,
child: Stack(
fit: StackFit.expand,
children: [
// Background image
img != null && img.isNotEmpty
? CachedNetworkImage(
imageUrl: img,
fit: BoxFit.cover,
width: double.infinity,
height: double.infinity,
placeholder: (_, __) => Container(
color: const Color(0xFF374151),
child: const Center(child: SizedBox(width: 24, height: 24, child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white38))),
),
errorWidget: (_, __, ___) => Container(
color: const Color(0xFF374151),
child: const Icon(Icons.image, color: Colors.white38, size: 40),
),
)
: Container(
color: const Color(0xFF374151),
child: const Icon(Icons.image, color: Colors.white38, size: 40),
),
// Dark gradient overlay at bottom
Positioned(
left: 0,
right: 0,
bottom: 0,
height: 80,
child: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Colors.transparent, Colors.black.withOpacity(0.7)],
),
),
),
),
// Title text at bottom left
Positioned(
left: 12,
right: 12,
bottom: 12,
child: Text(
event.title ?? event.name ?? '',
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
color: Colors.white,
fontSize: 14,
fontWeight: FontWeight.bold,
height: 1.2,
),
),
),
],
),
),
2026-01-31 15:23:18 +05:30
);
}
2026-03-11 20:13:13 +05:30
/// Build a type section that follows your requested layout rules:
/// - If type has <= 5 events => single horizontal row of compact cards.
/// - If type has >= 6 events => arrange events into column groups of 3 (so visually there are 3 rows across horizontally scrollable columns).
Widget _buildTypeSection(EventTypeModel type) {
final theme = Theme.of(context);
final eventsForType = _filteredEvents.where((e) => e.eventTypeId == type.id).toList();
2026-03-11 20:13:13 +05:30
final n = eventsForType.length;
2026-01-31 15:23:18 +05:30
2026-03-11 20:13:13 +05:30
// Header row
Widget header = Padding(
padding: const EdgeInsets.symmetric(horizontal: 4),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(type.name, style: theme.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold)),
TextButton(
onPressed: () {
_onSelectType(type.id);
},
child: Text('View All', style: TextStyle(color: theme.colorScheme.primary, fontWeight: FontWeight.w600)),
),
],
),
);
2026-01-31 15:23:18 +05:30
2026-03-11 20:13:13 +05:30
// If <= 5 events: show one horizontal row using _buildHorizontalEventCard
if (n <= 5) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
header,
const SizedBox(height: 8),
SizedBox(
height: 290, // card height: image 180 + text ~110
child: ListView.separated(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 8),
itemBuilder: (ctx, idx) => _buildHorizontalEventCard(eventsForType[idx]),
separatorBuilder: (_, __) => const SizedBox(width: 12),
itemCount: eventsForType.length,
),
),
],
);
2026-01-31 15:23:18 +05:30
}
2026-03-11 20:13:13 +05:30
// For 6+ events: arrange into columns where each column has up to 3 stacked cards.
final columnsCount = (n / 3).ceil();
final columnWidth = 260.0; // narrower so second column peeks in
final verticalCardHeight = 120.0; // each stacked card height matches sample
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
header,
const SizedBox(height: 8),
// Container height must accommodate 3 stacked cards + small gaps
SizedBox(
height: (verticalCardHeight * 3) + 16, // 3 cards + spacing
child: ListView.separated(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 8),
separatorBuilder: (_, __) => const SizedBox(width: 12),
itemBuilder: (ctx, colIndex) {
// Build one column: contains up to 3 items: indices colIndex*3 + 0/1/2
return Container(
width: columnWidth,
child: Column(
children: [
// top card
if ((colIndex * 3 + 0) < n)
SizedBox(
height: verticalCardHeight,
child: _buildStackedCard(eventsForType[colIndex * 3 + 0]),
)
else
const SizedBox(height: 0),
const SizedBox(height: 8),
// middle card
if ((colIndex * 3 + 1) < n)
SizedBox(
height: verticalCardHeight,
child: _buildStackedCard(eventsForType[colIndex * 3 + 1]),
)
else
const SizedBox(height: 0),
const SizedBox(height: 8),
// bottom card
if ((colIndex * 3 + 2) < n)
SizedBox(
height: verticalCardHeight,
child: _buildStackedCard(eventsForType[colIndex * 3 + 2]),
)
else
const SizedBox(height: 0),
],
),
);
},
itemCount: columnsCount,
),
),
],
);
2026-01-31 15:23:18 +05:30
}
2026-03-11 20:13:13 +05:30
/// A stacked card styled to match your sample (left square thumbnail, bold title).
/// REMOVED: price/rating row (per your request).
Widget _buildStackedCard(EventModel e) {
2026-01-31 15:23:18 +05:30
final theme = Theme.of(context);
String? img;
if (e.thumbImg != null && e.thumbImg!.isNotEmpty) {
img = e.thumbImg;
} else if (e.images.isNotEmpty && e.images.first.image.isNotEmpty) {
img = e.images.first.image;
}
return GestureDetector(
onTap: () {
2026-03-11 20:13:13 +05:30
if (e.id != null) Navigator.push(context, MaterialPageRoute(builder: (_) => LearnMoreScreen(eventId: e.id)));
2026-01-31 15:23:18 +05:30
},
child: Container(
2026-03-11 20:13:13 +05:30
margin: const EdgeInsets.symmetric(vertical: 0),
2026-01-31 15:23:18 +05:30
decoration: BoxDecoration(
color: theme.cardColor,
borderRadius: BorderRadius.circular(16),
2026-03-11 20:13:13 +05:30
boxShadow: [BoxShadow(color: theme.shadowColor.withOpacity(0.06), blurRadius: 12, offset: const Offset(0, 8))],
),
padding: const EdgeInsets.all(12),
child: Row(
children: [
// thumbnail square (rounded)
ClipRRect(
borderRadius: BorderRadius.circular(12),
child: img != null && img.isNotEmpty
? CachedNetworkImage(
imageUrl: img,
width: 96,
height: double.infinity,
fit: BoxFit.cover,
placeholder: (_, __) => Container(width: 96, color: theme.dividerColor, child: const Center(child: SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2)))),
errorWidget: (_, __, ___) => Container(width: 96, color: theme.dividerColor),
)
2026-03-11 20:13:13 +05:30
: Container(width: 96, height: double.infinity, color: theme.dividerColor),
),
const SizedBox(width: 14),
Expanded(
child: Column(crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.center, children: [
Text(e.title ?? e.name ?? '', maxLines: 2, overflow: TextOverflow.ellipsis, style: theme.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold, fontSize: 18)),
// removed price/rating row here per request
]),
),
// optional heart icon aligned top-right
Icon(Icons.favorite_border, color: theme.hintColor),
2026-01-31 15:23:18 +05:30
],
),
2026-03-11 20:13:13 +05:30
),
);
}
/// Compact card used inside the one-row layout for small counts (<=5).
/// Matches Figma: vertical card with image, date badge, title, location, "Free".
Widget _buildHorizontalEventCard(EventModel e) {
final theme = Theme.of(context);
String? img;
if (e.thumbImg != null && e.thumbImg!.isNotEmpty) {
img = e.thumbImg;
} else if (e.images.isNotEmpty && e.images.first.image.isNotEmpty) {
img = e.images.first.image;
}
// Parse day & month for the date badge
String day = '';
String month = '';
try {
final parts = e.startDate.split('-');
if (parts.length == 3) {
day = int.parse(parts[2]).toString();
const months = ['JAN','FEB','MAR','APR','MAY','JUN','JUL','AUG','SEP','OCT','NOV','DEC'];
month = months[int.parse(parts[1]) - 1];
}
} catch (_) {}
final venue = e.venueName ?? e.place ?? '';
return GestureDetector(
onTap: () {
if (e.id != null) Navigator.push(context, MaterialPageRoute(builder: (_) => LearnMoreScreen(eventId: e.id)));
},
child: SizedBox(
width: 220,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Image with date badge
Stack(
children: [
ClipRRect(
borderRadius: BorderRadius.circular(18),
child: img != null && img.isNotEmpty
? CachedNetworkImage(
imageUrl: img,
2026-03-11 20:13:13 +05:30
width: 220,
height: 180,
fit: BoxFit.cover,
placeholder: (_, __) => Container(
width: 220,
height: 180,
decoration: BoxDecoration(
color: theme.dividerColor,
borderRadius: BorderRadius.circular(18),
),
child: const Center(child: SizedBox(width: 24, height: 24, child: CircularProgressIndicator(strokeWidth: 2))),
),
errorWidget: (_, __, ___) => Container(
2026-03-11 20:13:13 +05:30
width: 220,
height: 180,
decoration: BoxDecoration(
color: theme.dividerColor,
borderRadius: BorderRadius.circular(18),
),
child: Icon(Icons.image, size: 40, color: theme.hintColor),
),
)
: Container(
width: 220,
height: 180,
decoration: BoxDecoration(
color: theme.dividerColor,
borderRadius: BorderRadius.circular(18),
),
child: Icon(Icons.image, size: 40, color: theme.hintColor),
),
),
// Date badge
if (day.isNotEmpty)
Positioned(
top: 10,
right: 10,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.08),
blurRadius: 6,
offset: const Offset(0, 2),
),
],
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
day,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w800,
color: Colors.black87,
height: 1.1,
),
),
Text(
month,
style: const TextStyle(
fontSize: 11,
fontWeight: FontWeight.w700,
color: Colors.black54,
height: 1.2,
),
),
],
),
),
),
],
),
const SizedBox(height: 10),
// Title
Text(
e.title ?? e.name ?? '',
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
if (venue.isNotEmpty) ...[
const SizedBox(height: 4),
2026-01-31 15:23:18 +05:30
Text(
2026-03-11 20:13:13 +05:30
venue,
maxLines: 1,
2026-01-31 15:23:18 +05:30
overflow: TextOverflow.ellipsis,
2026-03-11 20:13:13 +05:30
style: theme.textTheme.bodySmall?.copyWith(
color: theme.hintColor,
fontSize: 13,
),
2026-01-31 15:23:18 +05:30
),
2026-03-11 20:13:13 +05:30
],
const SizedBox(height: 4),
Text(
'Free',
style: TextStyle(
color: theme.colorScheme.primary,
fontWeight: FontWeight.w700,
fontSize: 14,
),
),
],
),
),
);
}
/// Format a date string (YYYY-MM-DD) to short display like "4 Mar".
String _formatDateShort(String dateStr) {
try {
final parts = dateStr.split('-');
if (parts.length == 3) {
final day = int.parse(parts[2]);
const months = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
final month = months[int.parse(parts[1]) - 1];
return '$day $month';
}
} catch (_) {}
return dateStr;
}
/// Full width card used when a single type is selected (vertical list).
/// Matches Figma: large image, badge, title, date + venue.
Widget _buildFullWidthCard(EventModel e) {
final theme = Theme.of(context);
String? img;
if (e.thumbImg != null && e.thumbImg!.isNotEmpty) {
img = e.thumbImg;
} else if (e.images.isNotEmpty && e.images.first.image.isNotEmpty) {
img = e.images.first.image;
}
// Build date range string
final startShort = _formatDateShort(e.startDate);
final endShort = _formatDateShort(e.endDate);
final dateRange = startShort == endShort ? startShort : '$startShort - $endShort';
final venue = e.venueName ?? e.place ?? '';
return GestureDetector(
onTap: () {
if (e.id != null) Navigator.push(context, MaterialPageRoute(builder: (_) => LearnMoreScreen(eventId: e.id)));
},
child: Container(
margin: const EdgeInsets.only(bottom: 18),
decoration: BoxDecoration(
color: theme.cardColor,
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(color: theme.shadowColor.withOpacity(0.10), blurRadius: 16, offset: const Offset(0, 6)),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Image with badge
Stack(
children: [
ClipRRect(
borderRadius: const BorderRadius.vertical(top: Radius.circular(20)),
child: img != null && img.isNotEmpty
? CachedNetworkImage(
imageUrl: img,
2026-03-11 20:13:13 +05:30
width: double.infinity,
height: 200,
fit: BoxFit.cover,
placeholder: (_, __) => Container(
width: double.infinity,
height: 200,
decoration: BoxDecoration(
color: theme.dividerColor,
borderRadius: const BorderRadius.vertical(top: Radius.circular(20)),
),
child: const Center(child: SizedBox(width: 28, height: 28, child: CircularProgressIndicator(strokeWidth: 2))),
),
errorWidget: (_, __, ___) => Container(
2026-03-11 20:13:13 +05:30
width: double.infinity,
height: 200,
decoration: BoxDecoration(
color: theme.dividerColor,
borderRadius: const BorderRadius.vertical(top: Radius.circular(20)),
),
child: Icon(Icons.image, size: 48, color: theme.hintColor),
),
)
: Container(
width: double.infinity,
height: 200,
decoration: BoxDecoration(
color: theme.dividerColor,
borderRadius: const BorderRadius.vertical(top: Radius.circular(20)),
),
child: Icon(Icons.image, size: 48, color: theme.hintColor),
),
),
// "ADDED BY EVENTIFY" badge
Positioned(
top: 14,
left: 14,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: theme.colorScheme.primary,
borderRadius: BorderRadius.circular(20),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: const [
Icon(Icons.star, color: Colors.white, size: 14),
SizedBox(width: 4),
Text(
'ADDED BY EVENTIFY',
style: TextStyle(
color: Colors.white,
fontSize: 11,
fontWeight: FontWeight.w700,
letterSpacing: 0.5,
),
),
],
2026-01-31 15:23:18 +05:30
),
),
2026-03-11 20:13:13 +05:30
),
],
),
// Title + date/venue
Padding(
padding: const EdgeInsets.fromLTRB(16, 14, 16, 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
e.title ?? e.name ?? '',
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
fontSize: 17,
2026-01-31 15:23:18 +05:30
),
2026-03-11 20:13:13 +05:30
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 8),
Row(
children: [
Icon(Icons.calendar_today_outlined, size: 14, color: theme.hintColor),
const SizedBox(width: 4),
Text(
dateRange,
style: theme.textTheme.bodySmall?.copyWith(
color: theme.hintColor,
fontSize: 13,
),
),
if (venue.isNotEmpty) ...[
const SizedBox(width: 12),
Icon(Icons.location_on_outlined, size: 14, color: theme.hintColor),
const SizedBox(width: 3),
Expanded(
child: Text(
venue,
style: theme.textTheme.bodySmall?.copyWith(
color: theme.hintColor,
fontSize: 13,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
],
],
2026-01-31 15:23:18 +05:30
),
],
),
2026-03-11 20:13:13 +05:30
),
],
),
2026-01-31 15:23:18 +05:30
),
);
}
2026-03-11 20:13:13 +05:30
IconData _getIconForType(String typeName) {
final name = typeName.toLowerCase();
if (name.contains('music')) return Icons.music_note;
if (name.contains('art') || name.contains('comedy')) return Icons.palette;
if (name.contains('festival')) return Icons.celebration;
if (name.contains('heritage') || name.contains('history')) return Icons.account_balance;
if (name.contains('sport')) return Icons.sports;
if (name.contains('food')) return Icons.restaurant;
return Icons.event;
}
void _onSelectType(int id) async {
setState(() {
_selectedTypeId = id;
});
try {
final all = await _eventsService.getEventsByPincode(_pincode);
final filtered = id == -1 ? all : all.where((e) => e.eventTypeId == id).toList();
if (mounted) setState(() { _events = filtered; _cachedFilteredEvents = null; });
2026-03-11 20:13:13 +05:30
} catch (e) {
if (mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(e.toString())));
}
}
2026-01-31 15:23:18 +05:30
String _getShortEmailLabel() {
try {
final parts = _username.split('@');
if (parts.isNotEmpty && parts[0].isNotEmpty) return parts[0];
} catch (_) {}
return 'You';
}
}