Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 5 additions & 23 deletions lib/widgets/drink_filter_sheets.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import 'package:provider/provider.dart';
import '../domain/models/models.dart';
import '../providers/providers.dart';
import '../utils/utils.dart';
import 'sheet_handle.dart';

/// Shows the category filter as a modal bottom sheet.
void showCategoryFilter(BuildContext context) {
Expand Down Expand Up @@ -32,25 +33,6 @@ void _showSheet(BuildContext context, WidgetBuilder builder) {
);
}

/// Drag handle shown at the top of every filter sheet.
class _SheetHandle extends StatelessWidget {
const _SheetHandle();

@override
Widget build(BuildContext context) {
return Center(
child: Container(
width: 32,
height: 4,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.onSurfaceVariant,
borderRadius: BorderRadius.circular(2),
),
),
);
}
}

/// Single-select category filter sheet.
class CategoryFilterSheet extends StatelessWidget {
final BeerProvider provider;
Expand All @@ -72,7 +54,7 @@ class CategoryFilterSheet extends StatelessWidget {
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const _SheetHandle(),
const SheetHandle(),
const SizedBox(height: 16),
Text('Filter by Category', style: theme.textTheme.titleLarge),
const SizedBox(height: 16),
Expand Down Expand Up @@ -152,7 +134,7 @@ class SortOptionsSheet extends StatelessWidget {
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const _SheetHandle(),
const SheetHandle(),
const SizedBox(height: 16),
Text('Sort By', style: theme.textTheme.titleLarge),
const SizedBox(height: 16),
Expand Down Expand Up @@ -220,7 +202,7 @@ class StyleFilterSheet extends StatelessWidget {
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const _SheetHandle(),
const SheetHandle(),
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
Expand Down Expand Up @@ -341,7 +323,7 @@ class VisibilityFilterSheet extends StatelessWidget {
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const _SheetHandle(),
const SheetHandle(),
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
Expand Down
66 changes: 50 additions & 16 deletions lib/widgets/festival_header.dart
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ class FestivalHeader extends StatelessWidget {
),
),
const SizedBox(width: 8),
FestivalStatusBadge(status: status),
FestivalStatusBadge(status: status, compact: true),
],
),
],
Expand All @@ -70,29 +70,46 @@ class FestivalHeader extends StatelessWidget {
}
}

/// Small coloured pill summarising a festival's [FestivalStatus]
/// (LIVE / SOON / RECENT / PAST). Colours adapt to light and dark themes.
/// Small coloured pill summarising a festival's [FestivalStatus].
///
/// [compact] selects both the label wording and the geometry:
/// - `compact: true` (app-bar header) — short labels (LIVE / SOON / RECENT /
/// PAST), tighter padding, smaller radius and font.
/// - `compact: false` (default, festival browser cards) — long labels
/// (LIVE / COMING SOON / MOST RECENT / PAST), roomier padding, larger
/// radius and font.
///
/// Colours adapt to light and dark themes and are identical for both modes.
class FestivalStatusBadge extends StatelessWidget {
const FestivalStatusBadge({required this.status, super.key});
const FestivalStatusBadge({
required this.status,
this.compact = false,
super.key,
});

final FestivalStatus status;
final bool compact;

@override
Widget build(BuildContext context) {
final isDark = Theme.of(context).brightness == Brightness.dark;
final (badgeLabel, _, lightColor, darkColor) = _styleFor(status);
final (compactLabel, longLabel, _, lightColor, darkColor) = _styleFor(
status,
);

return Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 1),
padding: compact
? const EdgeInsets.symmetric(horizontal: 6, vertical: 1)
: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: isDark ? darkColor : lightColor,
borderRadius: BorderRadius.circular(8),
borderRadius: BorderRadius.circular(compact ? 8 : 12),
),
child: Text(
badgeLabel,
style: const TextStyle(
compact ? compactLabel : longLabel,
style: TextStyle(
color: Colors.white,
fontSize: 9,
fontSize: compact ? 9 : 10,
fontWeight: FontWeight.bold,
),
),
Expand All @@ -101,30 +118,47 @@ class FestivalStatusBadge extends StatelessWidget {

/// Spoken form of the status for screen-reader labels (the badge text is
/// terse and is excluded from semantics at the parent level).
static String spokenLabel(FestivalStatus status) => _styleFor(status).$2;
static String spokenLabel(FestivalStatus status) => _styleFor(status).$3;

/// Returns the badge label, spoken label, and (light, dark) background
/// colours for [status]. Single source of truth for all status styling.
static (String, String, Color, Color) _styleFor(FestivalStatus status) {
/// Returns the compact label, long label, spoken label, and (light, dark)
/// background colours for [status]. Single source of truth for all status
/// styling, shared by the app-bar header and the festival browser cards.
static (String, String, String, Color, Color) _styleFor(
FestivalStatus status,
) {
switch (status) {
case FestivalStatus.live:
return const ('LIVE', 'live now', Color(0xFF2E7D32), Color(0xFF4CAF50));
return const (
'LIVE',
'LIVE',
'live now',
Color(0xFF2E7D32),
Color(0xFF4CAF50),
);
case FestivalStatus.upcoming:
return const (
'SOON',
'COMING SOON',
'starting soon',
Color(0xFF1976D2),
Color(0xFF42A5F5),
);
case FestivalStatus.mostRecent:
return const (
'RECENT',
'MOST RECENT',
'most recent',
Color(0xFFEF6C00),
Color(0xFFFF9800),
);
case FestivalStatus.past:
return const ('PAST', 'past', Color(0xFF616161), Color(0xFF9E9E9E));
return const (
'PAST',
'PAST',
'past',
Color(0xFF616161),
Color(0xFF9E9E9E),
);
}
}
}
93 changes: 7 additions & 86 deletions lib/widgets/festival_menu_sheets.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import 'package:provider/provider.dart';
import '../models/models.dart';
import '../providers/providers.dart';
import '../utils/utils.dart';
import 'festival_header.dart';
import 'sheet_handle.dart';

/// Shows the festival browser/selector as a modal bottom sheet
void showFestivalBrowser(BuildContext context) {
Expand Down Expand Up @@ -89,17 +91,7 @@ class FestivalSelectorSheet extends StatelessWidget {
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Center(
child: Container(
key: const Key('festival_selector_drag_handle'),
width: 32,
height: 4,
decoration: BoxDecoration(
color: theme.colorScheme.onSurfaceVariant,
borderRadius: BorderRadius.circular(2),
),
),
),
const SheetHandle(handleKey: Key('festival_selector_drag_handle')),
const SizedBox(height: 16),
Row(
children: [
Expand Down Expand Up @@ -300,7 +292,8 @@ class FestivalCard extends StatelessWidget {
children: [
Row(
children: [
_buildStatusBadge(status),
FestivalStatusBadge(status: status),
const SizedBox(width: 8),
Expanded(
child: Text(
festival.name,
Expand Down Expand Up @@ -422,58 +415,6 @@ class FestivalCard extends StatelessWidget {
),
);
}

Widget _buildStatusBadge(FestivalStatus status) {
return Builder(
builder: (context) {
final theme = Theme.of(context);
final isDark = theme.brightness == Brightness.dark;

Color backgroundColor;
String label;

switch (status) {
case FestivalStatus.live:
backgroundColor = isDark
? const Color(0xFF4CAF50)
: const Color(0xFF2E7D32);
label = 'LIVE';
case FestivalStatus.upcoming:
backgroundColor = isDark
? const Color(0xFF42A5F5)
: const Color(0xFF1976D2);
label = 'COMING SOON';
case FestivalStatus.mostRecent:
backgroundColor = isDark
? const Color(0xFFFF9800)
: const Color(0xFFEF6C00);
label = 'MOST RECENT';
case FestivalStatus.past:
backgroundColor = isDark
? const Color(0xFF9E9E9E)
: const Color(0xFF616161);
label = 'PAST';
}

return Container(
margin: const EdgeInsets.only(right: 8),
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: backgroundColor,
borderRadius: BorderRadius.circular(12),
),
child: Text(
label,
style: const TextStyle(
color: Colors.white,
fontSize: 10,
fontWeight: FontWeight.bold,
),
),
);
},
);
}
}

/// Settings bottom sheet with theme selector
Expand Down Expand Up @@ -508,17 +449,7 @@ class SettingsSheet extends StatelessWidget {
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Center(
child: Container(
key: const Key('settings_sheet_drag_handle'),
width: 32,
height: 4,
decoration: BoxDecoration(
color: theme.colorScheme.onSurfaceVariant,
borderRadius: BorderRadius.circular(2),
),
),
),
const SheetHandle(handleKey: Key('settings_sheet_drag_handle')),
const SizedBox(height: 16),
Text('Settings', style: theme.textTheme.titleLarge),
const SizedBox(height: 16),
Expand Down Expand Up @@ -569,17 +500,7 @@ class ThemeSelectorSheet extends StatelessWidget {
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Center(
child: Container(
key: const Key('theme_selector_sheet_drag_handle'),
width: 32,
height: 4,
decoration: BoxDecoration(
color: theme.colorScheme.onSurfaceVariant,
borderRadius: BorderRadius.circular(2),
),
),
),
const SheetHandle(handleKey: Key('theme_selector_sheet_drag_handle')),
const SizedBox(height: 16),
Text('Theme', style: theme.textTheme.titleLarge),
const SizedBox(height: 16),
Expand Down
31 changes: 31 additions & 0 deletions lib/widgets/sheet_handle.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import 'package:flutter/material.dart';

/// Drag handle shown at the top of every modal bottom sheet in this app
/// (filter sheets, festival selector, settings, theme selector).
///
/// [handleKey] is applied to the inner [Container] rather than to this widget,
/// so callers that key the drag handle for tests keep finding a [Container]
/// with that key — matching the pre-refactor widget tree, where the key lived
/// directly on the hand-rolled `Container`. Keying the widget itself via [key]
/// remains available and behaves normally.
class SheetHandle extends StatelessWidget {
const SheetHandle({this.handleKey, super.key});

/// Key applied to the inner container, for tests locating the handle.
final Key? handleKey;

@override
Widget build(BuildContext context) {
return Center(
child: Container(
key: handleKey,
width: 32,
height: 4,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.onSurfaceVariant,
borderRadius: BorderRadius.circular(2),
),
),
);
}
}
1 change: 1 addition & 0 deletions lib/widgets/widgets.dart
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export 'info_chip.dart';
export 'overflow_menu.dart';
export 'page_title.dart';
export 'section_header.dart';
export 'sheet_handle.dart';
export 'star_rating.dart';
export 'style_hero_panel.dart';
export 'your_take_card.dart';
Loading
Loading