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:
@@ -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>{};
|
||||
|
||||
Reference in New Issue
Block a user