diff --git a/mobile/meezi_app/lib/core/theme/app_theme.dart b/mobile/meezi_app/lib/core/theme/app_theme.dart new file mode 100644 index 0000000..78a3eff --- /dev/null +++ b/mobile/meezi_app/lib/core/theme/app_theme.dart @@ -0,0 +1,76 @@ +import 'package:flutter/material.dart'; + +/// Meezi brand palette. Green #0F6E56 matches the dashboard / Koja web. +class MeeziColors { + static const Color brand = Color(0xFF0F6E56); + static const Color brandDark = Color(0xFF0B5544); + static const Color accent = Color(0xFFE1F5EE); + static const Color surface = Color(0xFFF9FAFB); +} + +/// Centralized Meezi theme. Uses Vazirmatn when the font is bundled (see pubspec); +/// falls back to the platform font otherwise. Kept to stable Material 3 APIs. +class MeeziTheme { + static ThemeData light() { + final scheme = ColorScheme.fromSeed( + seedColor: MeeziColors.brand, + primary: MeeziColors.brand, + brightness: Brightness.light, + ); + return ThemeData( + useMaterial3: true, + colorScheme: scheme, + fontFamily: 'Vazirmatn', + scaffoldBackgroundColor: MeeziColors.surface, + appBarTheme: const AppBarTheme( + elevation: 0, + centerTitle: true, + backgroundColor: Colors.white, + foregroundColor: Colors.black87, + ), + filledButtonTheme: FilledButtonThemeData( + style: FilledButton.styleFrom( + backgroundColor: MeeziColors.brand, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 14, horizontal: 18), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + ), + ), + outlinedButtonTheme: OutlinedButtonThemeData( + style: OutlinedButton.styleFrom( + foregroundColor: MeeziColors.brand, + side: const BorderSide(color: MeeziColors.brand), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + ), + ), + inputDecorationTheme: InputDecorationTheme( + filled: true, + fillColor: Colors.white, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: Colors.black.withValues(alpha: 0.10)), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: Colors.black.withValues(alpha: 0.10)), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: const BorderSide(color: MeeziColors.brand, width: 1.5), + ), + ), + ); + } + + static ThemeData dark() { + final scheme = ColorScheme.fromSeed( + seedColor: MeeziColors.brand, + brightness: Brightness.dark, + ); + return ThemeData( + useMaterial3: true, + colorScheme: scheme, + fontFamily: 'Vazirmatn', + ); + } +} diff --git a/mobile/meezi_app/lib/features/public/public_api.dart b/mobile/meezi_app/lib/features/public/public_api.dart index 8e4ace6..9aff217 100644 --- a/mobile/meezi_app/lib/features/public/public_api.dart +++ b/mobile/meezi_app/lib/features/public/public_api.dart @@ -10,12 +10,28 @@ class PublicApi { String? q, double? minRating, String? sort, + List? themes, + List? vibes, + List? occasions, + List? spaceFeatures, + String? noise, + String? priceTier, + String? size, + bool openNow = false, }) async { final params = {}; 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>( '/api/public/discover', queryParameters: params.isEmpty ? null : params, @@ -24,6 +40,43 @@ class PublicApi { return list.cast>(); } + /// Cafés near a coordinate, sorted by distance (for "near me"). + Future>> discoverNearby({ + required double lat, + required double lng, + String? excludeSlug, + int limit = 12, + }) async { + final res = await _client.dio.get>( + '/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? ?? []; + return list.cast>(); + } + + /// Parse a free-text query into structured discovery hints (themes/vibes/...). + Future?> nlpParse(String q) async { + final res = await _client.dio.get>( + '/api/public/discover/nlp-parse', + queryParameters: {'q': q}, + ); + return res.data?['data'] as Map?; + } + + /// The discovery taxonomy (available themes, vibes, occasions, space features). + Future?> discoverTaxonomy() async { + final res = await _client.dio.get>( + '/api/public/discover-profile/taxonomy', + ); + return res.data?['data'] as Map?; + } + Future>> getReviews(String slug, {int page = 1}) async { final res = await _client.dio.get>( '/api/public/cafes/$slug/reviews', diff --git a/mobile/meezi_app/lib/main.dart b/mobile/meezi_app/lib/main.dart index bfd70fb..1f4ee53 100644 --- a/mobile/meezi_app/lib/main.dart +++ b/mobile/meezi_app/lib/main.dart @@ -3,6 +3,7 @@ import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'app/router.dart'; +import 'core/theme/app_theme.dart'; void main() { runApp(const ProviderScope(child: MeeziApp())); @@ -22,10 +23,9 @@ class MeeziApp extends StatelessWidget { GlobalWidgetsLocalizations.delegate, GlobalCupertinoLocalizations.delegate, ], - theme: ThemeData( - colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF6B4F3A)), - useMaterial3: true, - ), + theme: MeeziTheme.light(), + darkTheme: MeeziTheme.dark(), + themeMode: ThemeMode.light, routerConfig: appRouter, ); }