feat(meezi_app): Meezi green theme + rich discovery API (Koja parity, code-only)
CI/CD / CI · API (dotnet build + test) (push) Successful in 42s
CI/CD / CI · Admin API (dotnet build) (push) Successful in 37s
CI/CD / CI · Dashboard (tsc) (push) Successful in 1m5s
CI/CD / CI · Admin Web (tsc) (push) Successful in 38s
CI/CD / CI · Website (tsc) (push) Successful in 45s
CI/CD / CI · Koja (tsc) (push) Successful in 50s
CI/CD / Deploy · all services (push) Successful in 23s

Head-start on the Koja-Flutter build while pub access is unavailable (pub.dev 403
under sanctions). NOT yet built/verified — needs `flutter create` + `pub get` once
package access is restored.

- core/theme/app_theme.dart: centralized MeeziTheme (brand green #0F6E56, Material 3,
  filled/outlined buttons, inputs), wired into main.dart (was a brown seed, no theme).
- public_api.dart: discover() gains the full filter set (themes/vibes/occasions/
  spaceFeatures/noise/priceTier/size/openNow) + discoverNearby/nlpParse/discoverTaxonomy,
  matching the web Koja's backend surface. Follows the existing dio pattern.
This commit is contained in:
soroush.asadi
2026-06-03 07:33:12 +03:30
parent 45dab8b253
commit 1d79dde5e1
3 changed files with 133 additions and 4 deletions
@@ -10,12 +10,28 @@ class PublicApi {
String? q,
double? minRating,
String? sort,
List<String>? themes,
List<String>? vibes,
List<String>? occasions,
List<String>? spaceFeatures,
String? noise,
String? priceTier,
String? size,
bool openNow = false,
}) async {
final params = <String, String>{};
if (city != null && city.isNotEmpty) params['city'] = city;
if (q != null && q.isNotEmpty) params['q'] = q;
if (minRating != null) params['minRating'] = minRating.toString();
if (sort != null && sort.isNotEmpty) params['sort'] = sort;
if (themes != null && themes.isNotEmpty) params['themes'] = themes.join(',');
if (vibes != null && vibes.isNotEmpty) params['vibes'] = vibes.join(',');
if (occasions != null && occasions.isNotEmpty) params['occasions'] = occasions.join(',');
if (spaceFeatures != null && spaceFeatures.isNotEmpty) params['spaceFeatures'] = spaceFeatures.join(',');
if (noise != null && noise.isNotEmpty) params['noise'] = noise;
if (priceTier != null && priceTier.isNotEmpty) params['priceTier'] = priceTier;
if (size != null && size.isNotEmpty) params['size'] = size;
if (openNow) params['openNow'] = 'true';
final res = await _client.dio.get<Map<String, dynamic>>(
'/api/public/discover',
queryParameters: params.isEmpty ? null : params,
@@ -24,6 +40,43 @@ class PublicApi {
return list.cast<Map<String, dynamic>>();
}
/// Cafés near a coordinate, sorted by distance (for "near me").
Future<List<Map<String, dynamic>>> discoverNearby({
required double lat,
required double lng,
String? excludeSlug,
int limit = 12,
}) async {
final res = await _client.dio.get<Map<String, dynamic>>(
'/api/public/discover/near',
queryParameters: {
'lat': lat,
'lng': lng,
if (excludeSlug != null && excludeSlug.isNotEmpty) 'excludeSlug': excludeSlug,
'limit': limit,
},
);
final list = res.data?['data'] as List<dynamic>? ?? [];
return list.cast<Map<String, dynamic>>();
}
/// Parse a free-text query into structured discovery hints (themes/vibes/...).
Future<Map<String, dynamic>?> nlpParse(String q) async {
final res = await _client.dio.get<Map<String, dynamic>>(
'/api/public/discover/nlp-parse',
queryParameters: {'q': q},
);
return res.data?['data'] as Map<String, dynamic>?;
}
/// The discovery taxonomy (available themes, vibes, occasions, space features).
Future<Map<String, dynamic>?> discoverTaxonomy() async {
final res = await _client.dio.get<Map<String, dynamic>>(
'/api/public/discover-profile/taxonomy',
);
return res.data?['data'] as Map<String, dynamic>?;
}
Future<List<Map<String, dynamic>>> getReviews(String slug, {int page = 1}) async {
final res = await _client.dio.get<Map<String, dynamic>>(
'/api/public/cafes/$slug/reviews',