Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
41 changes: 37 additions & 4 deletions data/festivals.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,38 @@
{
"festivals": [
{
"id": "cbf2026",
"name": "Cambridge Beer Festival 2026",
"hashtag": "#cbf2026",
"start_date": "2026-05-18",
"end_date": "2026-05-23",
"location": "Jesus Green, Cambridge",
"address": "Jesus Green, Cambridge CB5 8BA",
"latitude": 52.2119,
"longitude": 0.1220,
"description": "The Cambridge Beer Festival 2026, featuring 200+ real ales from 100+ breweries, 80+ ciders and perries, international beer, mead, and wine.",
"website_url": "https://www.cambridgebeerfestival.com",
"hours": {
"Monday": "17:00 - 23:00",
"Tuesday": "12:00 - 23:00",
"Wednesday": "12:00 - 23:00",
"Thursday": "12:00 - 23:00",
"Friday": "12:00 - 23:00",
"Saturday": "12:00 - 22:00"
},
"available_beverage_types": [
"beer",
"international-beer",
"cider",
"perry",
"mead",
"wine",
"apple-juice",
"low-no"
],
"data_base_url": "/cbf2026",
"is_active": true
},
{
"id": "cbf2025",
"name": "Cambridge Beer Festival 2025",
Expand Down Expand Up @@ -31,7 +64,7 @@
"low-no"
],
"data_base_url": "/cbf2025",
"is_active": true
"is_active": false
},
{
"id": "cbfw2025",
Expand Down Expand Up @@ -91,7 +124,7 @@
"is_active": false
}
],
"default_festival_id": "cbf2025",
"version": "1.0.0",
"last_updated": "2025-11-29T00:00:00Z"
"default_festival_id": "cbf2026",
"version": "1.1.0",
"last_updated": "2026-05-03T00:00:00Z"
}
27 changes: 25 additions & 2 deletions lib/models/festival.dart
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,29 @@ class Festival {

/// Predefined festival configurations
class DefaultFestivals {
static final cambridge2026 = Festival(
id: 'cbf2026',
name: 'Cambridge Beer Festival 2026',
hashtag: '#cbf2026',
startDate: DateTime(2026, 5, 18),
endDate: DateTime(2026, 5, 23),
location: 'Jesus Green, Cambridge',
description: 'The Cambridge Beer Festival 2026',
websiteUrl: 'https://www.cambridgebeerfestival.com',
availableBeverageTypes: [
'beer',
'international-beer',
'cider',
'perry',
'mead',
'wine',
'apple-juice',
'low-no',
],
dataBaseUrl: 'https://data.cambeerfestival.app/cbf2026',
isActive: true,
Comment on lines +249 to +269
);

static final cambridge2025 = Festival(
id: 'cbf2025',
name: 'Cambridge Beer Festival 2025',
Expand All @@ -265,7 +288,7 @@ class DefaultFestivals {
'low-no',
],
dataBaseUrl: 'https://data.cambeerfestival.app/cbf2025',
isActive: true,
isActive: false,
);

static final cambridgeWinter2025 = Festival(
Expand Down Expand Up @@ -302,5 +325,5 @@ class DefaultFestivals {
isActive: false,
);

static List<Festival> get all => [cambridge2025, cambridgeWinter2025, cambridge2024];
static List<Festival> get all => [cambridge2026, cambridge2025, cambridgeWinter2025, cambridge2024];
}
4 changes: 2 additions & 2 deletions lib/providers/beer_provider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ class BeerProvider extends ChangeNotifier {
List<Festival> get festivals => _festivals;
/// Get festivals sorted by date (live/upcoming first, then past in reverse chronological order)
List<Festival> get sortedFestivals => Festival.sortByDate(_festivals);
Festival get currentFestival => _currentFestival ?? DefaultFestivals.cambridge2025;
Festival get currentFestival => _currentFestival ?? DefaultFestivals.all.firstWhere((f) => f.isActive, orElse: () => DefaultFestivals.all.first);
bool get isLoading => _isLoading;
bool get isFestivalsLoading => _isFestivalsLoading;
bool get isInitialized => _isInitialized;
Expand Down Expand Up @@ -243,7 +243,7 @@ class BeerProvider extends ChangeNotifier {
if (_festivals.isEmpty) {
await loadFestivals();
}
_currentFestival ??= DefaultFestivals.cambridge2025;
_currentFestival ??= DefaultFestivals.all.firstWhere((f) => f.isActive, orElse: () => DefaultFestivals.all.first);
}

_isLoading = true;
Expand Down
6 changes: 6 additions & 0 deletions mise.toml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,12 @@ description = "Run Flutter code analysis (no-fatal-infos mode)"
depends = ['generate']
run = 'flutter analyze --no-fatal-infos'

[tasks."validate:festivals"]
description = "Validate festivals.json against schema (mirrors CI)"
dir = "scripts"
sources = ['data/festivals.json', 'docs/code/api/festival-registry-schema.json']
run = 'npm ci && node ../scripts/validate-festivals.js'

[tasks."test:worker"]
description = "Run Cloudflare Worker tests (Vitest + workerd)"
dir = "cloudflare-worker"
Expand Down
2 changes: 1 addition & 1 deletion scripts/validate-festivals.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ const fs = require('fs');
const path = require('path');

// Paths relative to repo root
const schemaPath = path.join(__dirname, '..', 'docs', 'api', 'festival-registry-schema.json');
const schemaPath = path.join(__dirname, '..', 'docs', 'code', 'api', 'festival-registry-schema.json');
const festivalsPath = path.join(__dirname, '..', 'data', 'festivals.json');

// Load files
Expand Down
2 changes: 1 addition & 1 deletion test/beer_provider_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ void main() {
analyticsService: mockAnalyticsService,
);

expect(provider.currentFestival.id, 'cbf2025');
expect(provider.currentFestival.id, DefaultFestivals.all.firstWhere((f) => f.isActive).id);
});

test('isLoading is false initially', () {
Expand Down
18 changes: 16 additions & 2 deletions test/models_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -962,12 +962,25 @@ void main() {
});

group('DefaultFestivals', () {
test('cambridge2026 is configured correctly', () {
final festival = DefaultFestivals.cambridge2026;

expect(festival.id, 'cbf2026');
expect(festival.name, 'Cambridge Beer Festival 2026');
expect(festival.isActive, isTrue);
expect(festival.startDate, DateTime(2026, 5, 18));
expect(festival.endDate, DateTime(2026, 5, 23));
expect(festival.availableBeverageTypes, contains('beer'));
expect(festival.availableBeverageTypes, contains('cider'));
expect(festival.availableBeverageTypes, contains('mead'));
});

test('cambridge2025 is configured correctly', () {
final festival = DefaultFestivals.cambridge2025;

expect(festival.id, 'cbf2025');
expect(festival.name, 'Cambridge Beer Festival 2025');
expect(festival.isActive, isTrue);
expect(festival.isActive, isFalse);
expect(festival.availableBeverageTypes, contains('beer'));
expect(festival.availableBeverageTypes, contains('cider'));
expect(festival.availableBeverageTypes, contains('mead'));
Expand Down Expand Up @@ -996,7 +1009,8 @@ void main() {
test('all returns list of festivals', () {
final festivals = DefaultFestivals.all;

expect(festivals.length, 3);
expect(festivals.length, 4);
expect(festivals.map((f) => f.id), contains('cbf2026'));
Comment on lines +1012 to +1013
expect(festivals.map((f) => f.id), contains('cbf2025'));
expect(festivals.map((f) => f.id), contains('cbfw2025'));
expect(festivals.map((f) => f.id), contains('cbf2024'));
Expand Down
4 changes: 2 additions & 2 deletions test/router_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -306,7 +306,7 @@ void main() {
// Should fall back to default festival despite API failure
final currentUri = Uri.parse(appRouter.routerDelegate.currentConfiguration.uri.toString());
expect(currentUri.pathSegments.isNotEmpty, true);
expect(currentUri.pathSegments.first, 'cbf2025', reason: 'Should use default festival when API fails');
expect(currentUri.pathSegments.first, DefaultFestivals.all.firstWhere((f) => f.isActive).id, reason: 'Should use active hardcoded festival when API fails');
});

testWidgets('redirect handles empty festivals list', (tester) async {
Expand Down Expand Up @@ -335,7 +335,7 @@ void main() {
// Should use hardcoded default festival
final currentUri = Uri.parse(appRouter.routerDelegate.currentConfiguration.uri.toString());
expect(currentUri.pathSegments.isNotEmpty, true);
expect(currentUri.pathSegments.first, 'cbf2025', reason: 'Should use DefaultFestivals.cambridge2025');
expect(currentUri.pathSegments.first, DefaultFestivals.all.firstWhere((f) => f.isActive).id, reason: 'Should use active hardcoded festival when registry is empty');
});

testWidgets('multiple rapid navigations before init completes', (tester) async {
Expand Down
Loading