feat(meezi_app): café profile parity — cover, open badge, gallery, hours (code-only)
CI/CD / CI · API (dotnet build + test) (push) Successful in 41s
CI/CD / CI · Admin API (dotnet build) (push) Successful in 33s
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 22s

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.
This commit is contained in:
soroush.asadi
2026-06-03 08:00:22 +03:30
parent 2652736d31
commit af1794925d
@@ -93,11 +93,64 @@ class _CafeDetailScreenState extends ConsumerState<CafeDetailScreen> {
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()
: <String>[];
// WorkingHoursPublicDto: a day-keyed object {sat..fri}, each {isOpen,open,close}.
final hours = cafe['workingHours'] is Map
? (cafe['workingHours'] as Map)
: const <dynamic, dynamic>{};
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<CafeDetailScreen> {
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 <dynamic, dynamic>{};
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: [