Skip to content

Commit 05191ec

Browse files
committed
refactor: remove dead widgets and navigation helpers
Delete BottomActionBar/ActionButton, AvailabilityBadge, BreadcrumbBar, ABVStrengthHelper and four unused navigation helpers, along with the tests that were their only callers. buildDrinksPath and buildCategoryPath built routes the router never registered. Fixes #500 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017B31wKKL578bV18hcpXyE2
1 parent faba3e1 commit 05191ec

16 files changed

Lines changed: 9 additions & 1345 deletions

.claude/skills/ui-and-accessibility/SKILL.md

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -139,8 +139,6 @@ unless noted.
139139
| Divided strip of "key facts" (centred value over an uppercase label) inside a hero | `FactsStrip` + `FactCell` | `facts_strip.dart` | `FactsStrip` owns the top/bottom/left dividers; a `FactCell` becomes a navigation button when given `onTap` + `semanticLabel` |
140140
| Small metadata pill (style, dispense, bar location) | `InfoChip` | `info_chip.dart` | Optional `onTap` makes it a `Semantics(button: true)` link |
141141
| Section title with underline on a detail screen | `SectionHeader` | `section_header.dart` | `showSeparator` toggles the underline |
142-
| Sticky bottom row of actions (tasting log, rate, favourite, share) | `BottomActionBar` + `ActionButton` | `bottom_action_bar.dart` | **Currently unused** — no screen wires it up (the drink detail screen uses a FAB + `YourTakeCard` instead). Available, but check it still fits before adopting. `ActionButton.isActive` drives colour + `FontWeight`; `semanticLabel` overrides the visible label for screen readers |
143-
| Back-navigation header on a detail screen (drink/brewery/style) | `BreadcrumbBar` | `breadcrumb_bar.dart` | **Currently unused** — detail screens use `CollapsingDetailAppBar` + `buildHomeLeadingButton`. Only the `IconButton` gets `Semantics`, never the text row — see the "BAD" example in `docs/code/widget-standards.md`. 28px icon → 48×48 touch target. Text segments only become tappable/underlined when a callback is provided |
144142
| Three-dot menu for festival switch / settings / about | `buildOverflowMenu(context)` | `overflow_menu.dart` | A function, not a widget class — `docs/code/ui-components.md` documents where to include it (Drinks, My Festival screen) and where not to (detail screens, About, modals) |
145143
| Modal filter pickers (category, style, sort, visibility) | `showCategoryFilter` / `showStyleFilter` / `showSortOptions` / `showVisibilityFilter` | `drink_filter_sheets.dart` | All route through the private `_showSheet` helper (`isScrollControlled: true`) and share `_SheetHandle` — add a new filter type by adding a sheet class + show-function here, not a bespoke `showModalBottomSheet` call elsewhere |
146144
| Star rating display or picker | `StarRating` | `star_rating.dart` | `isEditable` toggles read-only vs tap-to-rate; semantic `value` is always `'$rating out of 5 stars'` |
@@ -245,8 +243,8 @@ ExcludeSemantics(child: Icon(Icons.festival, color: menuContentColor))
245243
`label` describing the *action*, not the icon (`'Add to favourites'`, not
246244
`'Heart icon'`).
247245
- Touch targets: 24×24 px minimum (WCAG AA); this project's icon buttons
248-
generally exceed that (`BreadcrumbBar`'s back button is 28px icon → 48×48
249-
effective target).
246+
generally exceed that (a typical back-navigation `IconButton` uses a 28px
247+
icon → 48×48 effective target).
250248
- Colour contrast 4.5:1 for text; never rely on colour alone to convey state
251249
(relevant to My Festival badges — see Part 6).
252250
- Don't wrap a whole `Row` containing both a button and plain text in one
@@ -331,10 +329,10 @@ navigateToRoute(context, buildBreweryPath(festivalId, breweryId));
331329
navigateToRoute(context, buildStylePath(festivalId, style)); // lowercases + encodes
332330
```
333331
Available builders (`lib/utils/navigation_helpers.dart`): `buildFestivalPath`,
334-
`buildFestivalHome`, `buildDrinksPath`, `buildFavoritesPath`,
335-
`buildFestivalInfoPath`, `buildDrinkDetailPath`, `buildBreweryPath`,
336-
`buildStylePath`, `buildCategoryPath`. Each asserts non-empty required
337-
arguments in debug mode and URL-encodes user-provided segments.
332+
`buildFestivalHome`, `buildFavoritesPath`, `buildFestivalInfoPath`,
333+
`buildDrinkDetailPath`, `buildBreweryPath`, `buildStylePath`. Each asserts
334+
non-empty required arguments in debug mode and URL-encodes user-provided
335+
segments.
338336

339337
### 4. Post-frame analytics in `initState`
340338

docs/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ How the codebase works -- implementation guides, architecture, and technical ref
1414
- **[routing.md](code/routing.md)** - URL routing (path-based with GoRouter)
1515
- **[navigation.md](code/navigation.md)** - Navigation helper API reference
1616
- **[widget-standards.md](code/widget-standards.md)** - Widget patterns and standards
17-
- **[ui-components.md](code/ui-components.md)** - Shared UI components (OverflowMenu, BreadcrumbBar)
17+
- **[ui-components.md](code/ui-components.md)** - Shared UI components (OverflowMenu)
1818
- **[network.md](code/network.md)** - Network security configuration and allowlist
1919
- **[api/](code/api/)** - API documentation
2020
- [README.md](code/api/README.md) - API overview

docs/code/navigation.md

Lines changed: 2 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,10 @@ All app URLs are scoped to a specific festival. This allows:
1616
```
1717

1818
Examples:
19-
- `/cbf2025` - Festival home
20-
- `/cbf2025/drinks` - Drinks list
19+
- `/cbf2025` - Festival home (drinks list)
2120
- `/cbf2025/drink/123` - Drink detail
2221
- `/cbf2025/brewery/456` - Brewery detail
2322
- `/cbf2025/style/ipa` - Style detail (lowercase canonical)
24-
- `/cbf2025/category/beer` - Category page
2523

2624
## URL Encoding
2725

@@ -43,38 +41,12 @@ import 'package:cambridge_beer_festival/utils/utils.dart';
4341
// Build festival home URL
4442
final homeUrl = buildFestivalHome('cbf2025'); // '/cbf2025'
4543
46-
// Build drinks URL
47-
final drinksUrl = buildDrinksPath('cbf2025'); // '/cbf2025/drinks'
48-
final beerUrl = buildDrinksPath('cbf2025', category: 'beer'); // '/cbf2025/drinks?category=beer'
49-
5044
// Build detail URLs
5145
final drinkUrl = buildDrinkDetailPath('cbf2025', drink.id);
5246
final breweryUrl = buildBreweryPath('cbf2025', brewery.id);
5347
final styleUrl = buildStylePath('cbf2025', 'IPA'); // Returns: '/cbf2025/style/ipa' (lowercase)
5448
```
5549

56-
### Parsing URLs
57-
58-
```dart
59-
// Extract festival ID from path
60-
final festivalId = extractFestivalId('/cbf2025/drinks'); // 'cbf2025'
61-
final festivalId2 = extractFestivalId('/cbf2025'); // 'cbf2025' (festival home)
62-
final festivalId3 = extractFestivalId('/'); // null
63-
final festivalId4 = extractFestivalId(''); // null
64-
65-
// Check if path is festival-scoped
66-
if (isFestivalPath(path)) {
67-
// Handle festival-scoped navigation
68-
}
69-
```
70-
71-
**Important Notes:**
72-
73-
- `extractFestivalId()` returns the first path segment, but **cannot validate** if it's a real festival ID
74-
- Single-segment paths like `/drinks` return `'drinks'` as the potential festival ID
75-
- Actual validation against the festival registry happens in Phase 1 routing logic
76-
- Empty paths and root path `/` return `null`
77-
7850
## Input Validation
7951

8052
All builder functions include assertions to prevent common errors:
@@ -83,17 +55,12 @@ All builder functions include assertions to prevent common errors:
8355
// ❌ These will throw AssertionError in debug mode:
8456
buildFestivalPath('', '/drinks'); // Empty festival ID
8557
buildDrinkDetailPath('cbf2025', ''); // Empty drink ID
86-
buildCategoryPath('cbf2025', ''); // Empty category
87-
88-
// ✅ These are handled gracefully:
89-
buildDrinksPath('cbf2025', category: ''); // Returns '/cbf2025/drinks' (no query param)
9058
```
9159

9260
## Testing
9361

9462
All navigation helpers have comprehensive test coverage in `test/utils/navigation_helpers_test.dart`:
9563
- URL encoding edge cases (special characters, Unicode, etc.)
9664
- Input validation (assertions)
97-
- Edge cases (long strings, multiple slashes, etc.)
65+
- Edge cases (long strings, etc.)
9866
- All builder functions
99-
- Path parsing and validation

docs/code/ui-components.md

Lines changed: 0 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -63,72 +63,3 @@ The overflow menu triggers these modal sheets:
6363
- `showSettingsSheet(context)` - Shows `SettingsSheet`
6464

6565
Both sheets are defined in `lib/widgets/festival_menu_sheets.dart`.
66-
67-
---
68-
69-
## BreadcrumbBar
70-
71-
A navigation breadcrumb bar for detail screens.
72-
73-
### Usage
74-
75-
```dart
76-
import 'package:cambridge_beer_festival/widgets/widgets.dart';
77-
import 'package:cambridge_beer_festival/utils/utils.dart';
78-
79-
// Breadcrumb for detail screens (back to festival home)
80-
BreadcrumbBar(
81-
backLabel: provider.currentFestival.id, // e.g., 'cbf2025'
82-
contextLabel: 'Oakham Ales',
83-
onBack: () {
84-
if (context.canPop()) {
85-
context.pop();
86-
} else {
87-
context.go(buildFestivalHome(festivalId));
88-
}
89-
},
90-
onBackLabelTap: () => context.go(buildFestivalHome(festivalId)),
91-
)
92-
```
93-
94-
**Current pattern (festival-scoped routing):**
95-
- `backLabel`: Festival ID (e.g., `cbf2025`, `cbf2024`)
96-
- `contextLabel`: Parent context (brewery name, style name, etc.)
97-
- `onBack`: Pop if possible, otherwise navigate to festival home
98-
- `onBackLabelTap`: Always navigate to festival home when clicking the festival ID
99-
100-
### Accessibility
101-
102-
- **Large touch target**: IconButton with 28px icon size (48x48 touch target)
103-
- **Semantic labels**: Only the IconButton has `Semantics` (not the entire row)
104-
- Label: "Back to {backLabel}"
105-
- Marked as button for screen readers
106-
- **Tooltip**: "Back to {backLabel}" on hover
107-
- **Text overflow handling**: Single line with ellipsis for long text
108-
- **Supports text scaling**: No overflow at 200% scale
109-
110-
### Implementation Details
111-
112-
- **Semantics structure**: Only the interactive IconButton is wrapped in `Semantics`
113-
- **Text widget**: Non-interactive text is NOT marked as a button
114-
- **Single-line constraint**: `maxLines: 1` with `TextOverflow.ellipsis`
115-
- **No variable shadowing**: Uses `contextLabel` property (not `context`) to avoid Flutter BuildContext confusion
116-
117-
### Design
118-
119-
- Material Design back arrow icon
120-
- Context text with separator (/)
121-
- Ellipsis for long text
122-
- Consistent padding (8px)
123-
124-
### When to Use
125-
126-
Use `BreadcrumbBar` on:
127-
- Drink detail screens (back to drinks list)
128-
- Brewery detail screens (back to drinks list)
129-
- Style detail screens (back to drinks list)
130-
131-
Do NOT use on:
132-
- Home screen (no parent)
133-
- Modal dialogs (use dialog close button)
134-
- Settings screens (use AppBar back button)

lib/utils/abv_strength_helper.dart

Lines changed: 0 additions & 53 deletions
This file was deleted.

lib/utils/navigation_helpers.dart

Lines changed: 0 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -39,25 +39,6 @@ String buildFestivalHome(String festivalId) {
3939
return '/$festivalId';
4040
}
4141

42-
/// Builds a drinks list URL for a festival.
43-
///
44-
/// The optional [category] parameter is URL-encoded to handle special characters.
45-
///
46-
/// Example:
47-
/// ```dart
48-
/// buildDrinksPath('cbf2025') // Returns: '/cbf2025/drinks'
49-
/// buildDrinksPath('cbf2025', category: 'beer') // Returns: '/cbf2025/drinks?category=beer'
50-
/// buildDrinksPath('cbf2025', category: 'cider & perry') // Returns: '/cbf2025/drinks?category=cider%20%26%20perry'
51-
/// ```
52-
String buildDrinksPath(String festivalId, {String? category}) {
53-
final base = buildFestivalPath(festivalId, '/drinks');
54-
if (category != null && category.isNotEmpty) {
55-
final encodedCategory = Uri.encodeQueryComponent(category);
56-
return '$base?category=$encodedCategory';
57-
}
58-
return base;
59-
}
60-
6142
/// Builds a favorites URL for a festival.
6243
///
6344
/// Example:
@@ -134,74 +115,6 @@ String buildStylePath(String festivalId, String style) {
134115
return buildFestivalPath(festivalId, '/style/$encodedStyle');
135116
}
136117

137-
/// Builds a category URL.
138-
///
139-
/// The [category] is URL-encoded to handle special characters safely.
140-
///
141-
/// Example:
142-
/// ```dart
143-
/// buildCategoryPath('cbf2025', 'beer') // Returns: '/cbf2025/category/beer'
144-
/// buildCategoryPath('cbf2025', 'low/no alcohol') // Returns: '/cbf2025/category/low%2Fno%20alcohol'
145-
/// ```
146-
String buildCategoryPath(String festivalId, String category) {
147-
assert(category.isNotEmpty, 'Category cannot be empty');
148-
final encodedCategory = Uri.encodeComponent(category);
149-
return buildFestivalPath(festivalId, '/category/$encodedCategory');
150-
}
151-
152-
/// Extracts festival ID from a festival-scoped path.
153-
///
154-
/// Returns the festival ID if the path follows the pattern `/{festivalId}/...`
155-
/// with at least one path segment after the festival ID. Returns `null` for
156-
/// non-festival-scoped paths.
157-
///
158-
/// A valid festival-scoped path must have at least 2 segments:
159-
/// - First segment: festival ID
160-
/// - Second+ segments: the actual route path
161-
///
162-
/// Example:
163-
/// ```dart
164-
/// extractFestivalId('/cbf2025/drinks') // Returns: 'cbf2025'
165-
/// extractFestivalId('/cbf2025/brewery/123') // Returns: 'cbf2025'
166-
/// extractFestivalId('/cbf2025') // Returns: 'cbf2025' (festival home is valid)
167-
/// extractFestivalId('/drinks') // Returns: null (not festival-scoped)
168-
/// extractFestivalId('/') // Returns: null
169-
/// extractFestivalId('') // Returns: null
170-
/// ```
171-
String? extractFestivalId(String path) {
172-
if (path.isEmpty) return null;
173-
174-
final segments = path.split('/').where((s) => s.isNotEmpty).toList();
175-
176-
// Need at least 1 segment for festival ID
177-
// Single segment like '/cbf2025' is valid (festival home)
178-
// Multiple segments like '/cbf2025/drinks' is valid
179-
if (segments.isEmpty) return null;
180-
181-
return segments.first;
182-
}
183-
184-
/// Checks if a path is festival-scoped.
185-
///
186-
/// A path is considered festival-scoped if it has at least one segment
187-
/// (the festival ID). This includes both festival home pages (`/cbf2025`)
188-
/// and nested routes (`/cbf2025/drinks`).
189-
///
190-
/// Example:
191-
/// ```dart
192-
/// isFestivalPath('/cbf2025/drinks') // Returns: true
193-
/// isFestivalPath('/cbf2025') // Returns: true
194-
/// isFestivalPath('/drinks') // Returns: true (single segment treated as potential festival ID)
195-
/// isFestivalPath('/') // Returns: false
196-
/// isFestivalPath('') // Returns: false
197-
/// ```
198-
///
199-
/// Note: This function cannot distinguish between a festival ID and a regular
200-
/// route without additional context. Use with caution for validation.
201-
bool isFestivalPath(String path) {
202-
return extractFestivalId(path) != null;
203-
}
204-
205118
/// Checks if navigation can pop in the current context.
206119
///
207120
/// Safely handles contexts where GoRouter may not be available (e.g., in tests).

lib/utils/utils.dart

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
export 'abv_strength_helper.dart';
21
export 'beverage_type_helper.dart';
32
export 'category_color_helper.dart';
43
export 'navigation_helpers.dart';

0 commit comments

Comments
 (0)