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
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:
@@ -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: [
|
||||
|
||||
Reference in New Issue
Block a user