feat: add bottom sheet for date filter chips on home screen

- Clicking Today/Tomorrow/This week opens a draggable bottom sheet
  showing filtered events matching the selected period
- Clicking Date opens calendar picker, then shows events for that date
- Bottom sheet matches web design: lavender bg, drag handle, title with
  count, close X button, scrollable event cards
- Event cards show image, title, date, location, and price label
- Sheet auto-clears filter on dismiss

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-14 20:15:55 +05:30
parent 5d9de1553d
commit 5b98f41596

View File

@@ -502,6 +502,9 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
_selectedCustomDate = picked;
_selectedDateFilter = 'Date';
});
_showFilteredEventsSheet(
'${picked.day.toString().padLeft(2, '0')} ${_monthName(picked.month)} ${picked.year}',
);
} else if (_selectedDateFilter == 'Date') {
setState(() {
_selectedDateFilter = '';
@@ -510,12 +513,261 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
}
} else {
setState(() {
_selectedDateFilter = _selectedDateFilter == label ? '' : label;
_selectedDateFilter = label;
_selectedCustomDate = null;
});
_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: Image.network(
imageUrl,
width: 80,
height: 80,
fit: BoxFit.cover,
errorBuilder: (_, __, ___) => 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),
);
}
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,
),
),
],
),
),
],
),
),
);
}
/// Collect all event dates (start + end range) to show dots on the calendar.
Set<DateTime> get _eventDates {
final dates = <DateTime>{};