From af1794925d87a6ddce5879db7bec71c3bd756cf8 Mon Sep 17 00:00:00 2001 From: "soroush.asadi" Date: Wed, 3 Jun 2026 08:00:22 +0330 Subject: [PATCH] =?UTF-8?q?feat(meezi=5Fapp):=20caf=C3=A9=20profile=20pari?= =?UTF-8?q?ty=20=E2=80=94=20cover,=20open=20badge,=20gallery,=20hours=20(c?= =?UTF-8?q?ode-only)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Enhances the café detail screen toward web-Koja parity. Parsing verified against the real backend DTOs (CafePublicDto / WorkingHoursPublicDto), still unbuilt (pub blocked). - Cover image hero (coverImageUrl), open/closed badge (isOpenNow). - Photo gallery (galleryUrls) horizontal strip. - Working hours rendered from the day-keyed WorkingHoursPublicDto ({sat..fri} of {isOpen,open,close}), Sat→Fri with Persian day labels. --- .../features/discover/cafe_detail_screen.dart | 108 +++++++++++++++++- 1 file changed, 107 insertions(+), 1 deletion(-) diff --git a/mobile/meezi_app/lib/features/discover/cafe_detail_screen.dart b/mobile/meezi_app/lib/features/discover/cafe_detail_screen.dart index fa0bfe3..964f33b 100644 --- a/mobile/meezi_app/lib/features/discover/cafe_detail_screen.dart +++ b/mobile/meezi_app/lib/features/discover/cafe_detail_screen.dart @@ -93,11 +93,64 @@ class _CafeDetailScreenState extends ConsumerState { final description = cafe['description'] as String?; final address = cafe['address'] as String?; final city = cafe['city'] as String?; + // Defensive parsing — public DTO key names may vary. + final cover = (cafe['coverImageUrl'] ?? cafe['coverUrl'] ?? cafe['cover']) as String?; + final isOpen = cafe['isOpenNow'] as bool?; + final gallery = (cafe['galleryUrls'] ?? cafe['gallery']) is List + ? ((cafe['galleryUrls'] ?? cafe['gallery']) as List) + .map((e) => e.toString()) + .where((e) => e.isNotEmpty) + .toList() + : []; + // WorkingHoursPublicDto: a day-keyed object {sat..fri}, each {isOpen,open,close}. + final hours = cafe['workingHours'] is Map + ? (cafe['workingHours'] as Map) + : const {}; return ListView( padding: const EdgeInsets.all(16), children: [ - Text(name, style: Theme.of(context).textTheme.headlineSmall), + if (cover != null && cover.isNotEmpty) ...[ + ClipRRect( + borderRadius: BorderRadius.circular(16), + child: AspectRatio( + aspectRatio: 16 / 9, + child: Image.network( + cover, + fit: BoxFit.cover, + errorBuilder: (_, __, ___) => + Container(color: Colors.black12), + ), + ), + ), + const SizedBox(height: 12), + ], + Row( + children: [ + Expanded( + child: Text(name, + style: Theme.of(context).textTheme.headlineSmall), + ), + if (isOpen != null) + Container( + padding: const EdgeInsets.symmetric( + horizontal: 10, vertical: 4), + decoration: BoxDecoration( + color: (isOpen ? Colors.green : Colors.red) + .withValues(alpha: 0.12), + borderRadius: BorderRadius.circular(20), + ), + child: Text( + isOpen ? 'باز است' : 'بسته است', + style: TextStyle( + color: isOpen ? Colors.green[800] : Colors.red[800], + fontWeight: FontWeight.bold, + fontSize: 12, + ), + ), + ), + ], + ), const SizedBox(height: 8), Row( children: [ @@ -117,6 +170,59 @@ class _CafeDetailScreenState extends ConsumerState { const SizedBox(height: 12), Text(description), ], + if (gallery.isNotEmpty) ...[ + const SizedBox(height: 12), + SizedBox( + height: 110, + child: ListView.separated( + scrollDirection: Axis.horizontal, + itemCount: gallery.length, + separatorBuilder: (_, __) => const SizedBox(width: 8), + itemBuilder: (_, i) => ClipRRect( + borderRadius: BorderRadius.circular(12), + child: Image.network( + gallery[i], + width: 150, + fit: BoxFit.cover, + errorBuilder: (_, __, ___) => + Container(width: 150, color: Colors.black12), + ), + ), + ), + ), + ], + if (hours.isNotEmpty) ...[ + const SizedBox(height: 16), + Text('ساعات کاری', + style: Theme.of(context).textTheme.titleMedium), + const SizedBox(height: 8), + ...const [ + ('sat', 'شنبه'), + ('sun', 'یکشنبه'), + ('mon', 'دوشنبه'), + ('tue', 'سه‌شنبه'), + ('wed', 'چهارشنبه'), + ('thu', 'پنجشنبه'), + ('fri', 'جمعه'), + ].map((d) { + final m = hours[d.$1] is Map + ? hours[d.$1] as Map + : const {}; + final open = (m['open'] ?? '').toString(); + final close = (m['close'] ?? '').toString(); + final isOpen = m['isOpen'] == true && open.isNotEmpty; + return Padding( + padding: const EdgeInsets.symmetric(vertical: 2), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(d.$2), + Text(isOpen ? '$open - $close' : 'تعطیل'), + ], + ), + ); + }), + ], const SizedBox(height: 16), Row( children: [