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;
|
_selectedCustomDate = picked;
|
||||||
_selectedDateFilter = 'Date';
|
_selectedDateFilter = 'Date';
|
||||||
});
|
});
|
||||||
|
_showFilteredEventsSheet(
|
||||||
|
'${picked.day.toString().padLeft(2, '0')} ${_monthName(picked.month)} ${picked.year}',
|
||||||
|
);
|
||||||
} else if (_selectedDateFilter == 'Date') {
|
} else if (_selectedDateFilter == 'Date') {
|
||||||
setState(() {
|
setState(() {
|
||||||
_selectedDateFilter = '';
|
_selectedDateFilter = '';
|
||||||
@@ -510,12 +513,261 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
setState(() {
|
setState(() {
|
||||||
_selectedDateFilter = _selectedDateFilter == label ? '' : label;
|
_selectedDateFilter = label;
|
||||||
_selectedCustomDate = null;
|
_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.
|
/// Collect all event dates (start + end range) to show dots on the calendar.
|
||||||
Set<DateTime> get _eventDates {
|
Set<DateTime> get _eventDates {
|
||||||
final dates = <DateTime>{};
|
final dates = <DateTime>{};
|
||||||
|
|||||||
Reference in New Issue
Block a user