-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathnavigation_helpers.dart
More file actions
218 lines (201 loc) · 7.41 KB
/
Copy pathnavigation_helpers.dart
File metadata and controls
218 lines (201 loc) · 7.41 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
/// Navigation utilities for festival-scoped routing.
///
/// Provides helper functions to build festival-scoped URLs consistently
/// throughout the app. These will be used in Phase 1 when routes are updated.
///
/// All functions perform URL encoding where appropriate to handle special
/// characters safely.
library;
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
/// Builds a festival-scoped URL path.
///
/// The [festivalId] and [path] must not be empty.
///
/// Example:
/// ```dart
/// buildFestivalPath('cbf2025', '/drinks') // Returns: '/cbf2025/drinks'
/// buildFestivalPath('cbf2025', '/brewery/123') // Returns: '/cbf2025/brewery/123'
/// ```
String buildFestivalPath(String festivalId, String path) {
assert(festivalId.isNotEmpty, 'Festival ID cannot be empty');
assert(path.isNotEmpty, 'Path cannot be empty');
// Ensure path starts with /
final cleanPath = path.startsWith('/') ? path : '/$path';
return '/$festivalId$cleanPath';
}
/// Builds a festival home URL.
///
/// Example:
/// ```dart
/// buildFestivalHome('cbf2025') // Returns: '/cbf2025'
/// ```
String buildFestivalHome(String festivalId) {
assert(festivalId.isNotEmpty, 'Festival ID cannot be empty');
return '/$festivalId';
}
/// Builds a drinks list URL for a festival.
///
/// The optional [category] parameter is URL-encoded to handle special characters.
///
/// Example:
/// ```dart
/// buildDrinksPath('cbf2025') // Returns: '/cbf2025/drinks'
/// buildDrinksPath('cbf2025', category: 'beer') // Returns: '/cbf2025/drinks?category=beer'
/// buildDrinksPath('cbf2025', category: 'cider & perry') // Returns: '/cbf2025/drinks?category=cider%20%26%20perry'
/// ```
String buildDrinksPath(String festivalId, {String? category}) {
final base = buildFestivalPath(festivalId, '/drinks');
if (category != null && category.isNotEmpty) {
final encodedCategory = Uri.encodeQueryComponent(category);
return '$base?category=$encodedCategory';
}
return base;
}
/// Builds a favorites URL for a festival.
///
/// Example:
/// ```dart
/// buildFavoritesPath('cbf2025') // Returns: '/cbf2025/favorites'
/// ```
String buildFavoritesPath(String festivalId) {
return buildFestivalPath(festivalId, '/favorites');
}
/// Builds a festival info URL.
///
/// Example:
/// ```dart
/// buildFestivalInfoPath('cbf2025') // Returns: '/cbf2025/info'
/// ```
String buildFestivalInfoPath(String festivalId) {
return buildFestivalPath(festivalId, '/info');
}
/// Builds a drink detail URL.
///
/// The [drinkId] is URL-encoded to handle special characters safely.
///
/// Example:
/// ```dart
/// buildDrinkDetailPath('cbf2025', 'drink-123') // Returns: '/cbf2025/drink/drink-123'
/// buildDrinkDetailPath('cbf2025', 'drink 456') // Returns: '/cbf2025/drink/drink%20456'
/// ```
String buildDrinkDetailPath(String festivalId, String drinkId) {
assert(drinkId.isNotEmpty, 'Drink ID cannot be empty');
final encodedId = Uri.encodeComponent(drinkId);
return buildFestivalPath(festivalId, '/drink/$encodedId');
}
/// Builds a brewery detail URL.
///
/// The [breweryId] is URL-encoded to handle special characters safely.
///
/// Example:
/// ```dart
/// buildBreweryPath('cbf2025', 'brewery-123') // Returns: '/cbf2025/brewery/brewery-123'
/// buildBreweryPath('cbf2025', 'oak & elm') // Returns: '/cbf2025/brewery/oak%20%26%20elm'
/// ```
String buildBreweryPath(String festivalId, String breweryId) {
assert(breweryId.isNotEmpty, 'Brewery ID cannot be empty');
final encodedId = Uri.encodeComponent(breweryId);
return buildFestivalPath(festivalId, '/brewery/$encodedId');
}
/// Builds a style detail URL with lowercase canonical format.
///
/// The style name is converted to lowercase for canonical URLs.
/// This improves SEO and ensures consistent URL format.
///
/// Example:
/// ```dart
/// buildStylePath('cbf2025', 'IPA') // Returns: '/cbf2025/style/ipa'
/// buildStylePath('cbf2025', 'American IPA') // Returns: '/cbf2025/style/american%20ipa'
/// ```
String buildStylePath(String festivalId, String style) {
assert(style.isNotEmpty, 'Style cannot be empty');
// Convert to lowercase for canonical URLs
final lowercaseStyle = style.toLowerCase();
// URL-encode the style name to handle special characters
final encodedStyle = Uri.encodeComponent(lowercaseStyle);
return buildFestivalPath(festivalId, '/style/$encodedStyle');
}
/// Builds a category URL.
///
/// The [category] is URL-encoded to handle special characters safely.
///
/// Example:
/// ```dart
/// buildCategoryPath('cbf2025', 'beer') // Returns: '/cbf2025/category/beer'
/// buildCategoryPath('cbf2025', 'low/no alcohol') // Returns: '/cbf2025/category/low%2Fno%20alcohol'
/// ```
String buildCategoryPath(String festivalId, String category) {
assert(category.isNotEmpty, 'Category cannot be empty');
final encodedCategory = Uri.encodeComponent(category);
return buildFestivalPath(festivalId, '/category/$encodedCategory');
}
/// Extracts festival ID from a festival-scoped path.
///
/// Returns the festival ID if the path follows the pattern `/{festivalId}/...`
/// with at least one path segment after the festival ID. Returns `null` for
/// non-festival-scoped paths.
///
/// A valid festival-scoped path must have at least 2 segments:
/// - First segment: festival ID
/// - Second+ segments: the actual route path
///
/// Example:
/// ```dart
/// extractFestivalId('/cbf2025/drinks') // Returns: 'cbf2025'
/// extractFestivalId('/cbf2025/brewery/123') // Returns: 'cbf2025'
/// extractFestivalId('/cbf2025') // Returns: 'cbf2025' (festival home is valid)
/// extractFestivalId('/drinks') // Returns: null (not festival-scoped)
/// extractFestivalId('/') // Returns: null
/// extractFestivalId('') // Returns: null
/// ```
String? extractFestivalId(String path) {
if (path.isEmpty) return null;
final segments = path.split('/').where((s) => s.isNotEmpty).toList();
// Need at least 1 segment for festival ID
// Single segment like '/cbf2025' is valid (festival home)
// Multiple segments like '/cbf2025/drinks' is valid
if (segments.isEmpty) return null;
return segments.first;
}
/// Checks if a path is festival-scoped.
///
/// A path is considered festival-scoped if it has at least one segment
/// (the festival ID). This includes both festival home pages (`/cbf2025`)
/// and nested routes (`/cbf2025/drinks`).
///
/// Example:
/// ```dart
/// isFestivalPath('/cbf2025/drinks') // Returns: true
/// isFestivalPath('/cbf2025') // Returns: true
/// isFestivalPath('/drinks') // Returns: true (single segment treated as potential festival ID)
/// isFestivalPath('/') // Returns: false
/// isFestivalPath('') // Returns: false
/// ```
///
/// Note: This function cannot distinguish between a festival ID and a regular
/// route without additional context. Use with caution for validation.
bool isFestivalPath(String path) {
return extractFestivalId(path) != null;
}
/// Checks if navigation can pop in the current context.
///
/// Safely handles contexts where GoRouter may not be available (e.g., in tests).
/// Returns `true` if the router can navigate back, `false` otherwise.
///
/// This is useful for determining whether to show a back button or a home button
/// in the app bar.
///
/// Example:
/// ```dart
/// final canPop = canPopNavigation(context);
/// leading: canPop ? null : IconButton(icon: Icon(Icons.home), ...)
/// ```
bool canPopNavigation(BuildContext context) {
try {
return GoRouter.of(context).canPop();
} catch (e) {
// GoRouter not available (e.g., in tests)
return false;
}
}