feat: fix View All buttons and category selection UX
- "View All" on "Events Around You" header now toggles between horizontal scroll and expanded wrap grid showing all categories - Tapping a category chip replaces all shelf sections with a filtered vertical list of events for that category only - Tapping "All Events" restores the shelf layout for all categories - "View All" on each shelf header (Music, Festivals, etc.) selects that category in the chips and shows its filtered event list - Added AnimatedSwitcher for smooth transition between views - Added AnimatedCrossFade for chip expand/collapse animation Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -40,6 +40,7 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
|
|||||||
List<EventModel> _events = [];
|
List<EventModel> _events = [];
|
||||||
List<EventTypeModel> _types = [];
|
List<EventTypeModel> _types = [];
|
||||||
int _selectedTypeId = -1; // -1 == All
|
int _selectedTypeId = -1; // -1 == All
|
||||||
|
bool _categoriesExpanded = false;
|
||||||
|
|
||||||
bool _loading = true;
|
bool _loading = true;
|
||||||
|
|
||||||
@@ -1579,21 +1580,50 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () {},
|
onPressed: () => setState(() => _categoriesExpanded = !_categoriesExpanded),
|
||||||
child: const Text(
|
child: Text(
|
||||||
'View All',
|
_categoriesExpanded ? 'Show Less' : 'View All',
|
||||||
style: TextStyle(color: Color(0xFF2563EB), fontWeight: FontWeight.w600),
|
style: const TextStyle(color: Color(0xFF2563EB), fontWeight: FontWeight.w600),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
|
|
||||||
// Category chips (card-style)
|
// Category chips — horizontal scroll (collapsed) or wrap grid (expanded)
|
||||||
SizedBox(
|
AnimatedCrossFade(
|
||||||
height: 140,
|
duration: const Duration(milliseconds: 300),
|
||||||
child: ListView(
|
crossFadeState: _categoriesExpanded ? CrossFadeState.showSecond : CrossFadeState.showFirst,
|
||||||
scrollDirection: Axis.horizontal,
|
// Collapsed: horizontal scroll
|
||||||
|
firstChild: SizedBox(
|
||||||
|
height: 140,
|
||||||
|
child: ListView(
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
children: [
|
||||||
|
_categoryChip(
|
||||||
|
label: 'All Events',
|
||||||
|
icon: Icons.grid_view_rounded,
|
||||||
|
selected: _selectedTypeId == -1,
|
||||||
|
onTap: () => _onSelectType(-1),
|
||||||
|
),
|
||||||
|
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),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// Expanded: wrap grid showing all categories
|
||||||
|
secondChild: Wrap(
|
||||||
|
spacing: 10,
|
||||||
|
runSpacing: 10,
|
||||||
children: [
|
children: [
|
||||||
_categoryChip(
|
_categoryChip(
|
||||||
label: 'All Events',
|
label: 'All Events',
|
||||||
@@ -1601,8 +1631,7 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
|
|||||||
selected: _selectedTypeId == -1,
|
selected: _selectedTypeId == -1,
|
||||||
onTap: () => _onSelectType(-1),
|
onTap: () => _onSelectType(-1),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 12),
|
for (final t in _types)
|
||||||
for (final t in _types) ...[
|
|
||||||
_categoryChip(
|
_categoryChip(
|
||||||
label: t.name,
|
label: t.name,
|
||||||
imageUrl: t.iconUrl,
|
imageUrl: t.iconUrl,
|
||||||
@@ -1610,14 +1639,12 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
|
|||||||
selected: _selectedTypeId == t.id,
|
selected: _selectedTypeId == t.id,
|
||||||
onTap: () => _onSelectType(t.id),
|
onTap: () => _onSelectType(t.id),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 12),
|
|
||||||
],
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
// Event sections by type — always show ALL categories
|
// Event sections — shelves (All) or filtered list (specific category)
|
||||||
if (_loading)
|
if (_loading)
|
||||||
const Padding(
|
const Padding(
|
||||||
padding: EdgeInsets.all(40),
|
padding: EdgeInsets.all(40),
|
||||||
@@ -1632,14 +1659,55 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
|
|||||||
)),
|
)),
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
Column(
|
AnimatedSwitcher(
|
||||||
children: [
|
duration: const Duration(milliseconds: 300),
|
||||||
for (final t in _types)
|
switchInCurve: Curves.easeOut,
|
||||||
if (_allFilteredByDate.where((e) => e.eventTypeId == t.id).isNotEmpty) ...[
|
switchOutCurve: Curves.easeIn,
|
||||||
_buildTypeSection(t),
|
child: _selectedTypeId == -1
|
||||||
const SizedBox(height: 18),
|
// "All Events" — show category shelf sections
|
||||||
],
|
? Column(
|
||||||
],
|
key: const ValueKey('shelves'),
|
||||||
|
children: [
|
||||||
|
for (final t in _types)
|
||||||
|
if (_allFilteredByDate.where((e) => e.eventTypeId == t.id).isNotEmpty) ...[
|
||||||
|
_buildTypeSection(t),
|
||||||
|
const SizedBox(height: 18),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
)
|
||||||
|
// Specific category — show vertical list of events for that category only
|
||||||
|
: Column(
|
||||||
|
key: ValueKey('filtered_$_selectedTypeId'),
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// Category name header
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 14),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
_types.firstWhere((t) => t.id == _selectedTypeId, orElse: () => _types.first).name,
|
||||||
|
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Color(0xFF111827)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'${_allFilteredByDate.where((e) => e.eventTypeId == _selectedTypeId).length} events',
|
||||||
|
style: const TextStyle(fontSize: 13, color: Color(0xFF9CA3AF)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// Event cards
|
||||||
|
for (final e in _allFilteredByDate.where((e) => e.eventTypeId == _selectedTypeId))
|
||||||
|
_buildFullWidthCard(e),
|
||||||
|
if (_allFilteredByDate.where((e) => e.eventTypeId == _selectedTypeId).isEmpty)
|
||||||
|
const Padding(
|
||||||
|
padding: EdgeInsets.all(40),
|
||||||
|
child: Center(child: Text('No events in this category', style: TextStyle(color: Color(0xFF9CA3AF)))),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
// Bottom padding for nav bar
|
// Bottom padding for nav bar
|
||||||
|
|||||||
Reference in New Issue
Block a user