import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:anyway/domain/entities/landmark.dart'; import 'package:anyway/domain/entities/landmark_type.dart'; class TripLandmarkCard extends StatelessWidget { const TripLandmarkCard({super.key, required this.landmark, required this.position, required this.onToggleVisited, this.onOpenWebsite}); final Landmark landmark; final int position; final VoidCallback onToggleVisited; final VoidCallback? onOpenWebsite; @override Widget build(BuildContext context) { final visited = landmark.isVisited; final metaItems = _buildMetaItems( duration: landmark.durationMinutes, reachTime: landmark.timeToReachNextMinutes, tags: landmark.description?.tags ?? const [], tagCount: landmark.tagCount, attractiveness: landmark.attractiveness, isSecondary: landmark.isSecondary ?? false, websiteLabel: landmark.websiteUrl, onOpenWebsite: onOpenWebsite, ); return Container( margin: const EdgeInsets.only(bottom: 10), child: Card( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), elevation: 4, clipBehavior: Clip.antiAlias, child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ _LandmarkMedia(landmark: landmark, position: position, type: landmark.type.type), Expanded( child: Padding( padding: const EdgeInsets.fromLTRB(12, 12, 14, 12), child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( child: Text( landmark.name, maxLines: 2, overflow: TextOverflow.ellipsis, style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w600), ), ), IconButton( iconSize: 22, padding: EdgeInsets.zero, splashRadius: 18, tooltip: visited ? 'Mark as pending' : 'Mark visited', onPressed: onToggleVisited, icon: Icon(visited ? Icons.radio_button_checked : Icons.radio_button_unchecked, color: visited ? Colors.green : Colors.grey), ), ], ), if (landmark.nameEn != null) Padding( padding: const EdgeInsets.only(top: 2), child: Text( landmark.nameEn!, maxLines: 1, overflow: TextOverflow.ellipsis, style: Theme.of(context).textTheme.labelMedium?.copyWith(color: Colors.grey.shade600), ), ), if (landmark.description?.description != null) Padding( padding: const EdgeInsets.only(top: 8), child: Text(landmark.description!.description, maxLines: 2, overflow: TextOverflow.ellipsis, style: Theme.of(context).textTheme.bodyMedium?.copyWith(height: 1.3)), ), if (metaItems.isNotEmpty) ...[const SizedBox(height: 10), _MetaScroller(items: metaItems)], ], ), ), ), ], ), ), ); } List<_MetaItem> _buildMetaItems({ int? duration, int? reachTime, List tags = const [], int? tagCount, int? attractiveness, bool isSecondary = false, String? websiteLabel, VoidCallback? onOpenWebsite, }) { final items = <_MetaItem>[]; if (duration != null) { items.add(_MetaItem(icon: Icons.timer_outlined, label: '$duration min stay')); } if (reachTime != null) { items.add(_MetaItem(icon: Icons.directions_walk, label: '$reachTime min walk')); } if (attractiveness != null) { items.add(_MetaItem(icon: Icons.star, label: 'Score $attractiveness')); } if (tagCount != null) { items.add(_MetaItem(icon: Icons.label, label: '$tagCount tags')); } if (isSecondary) { items.add(_MetaItem(icon: Icons.layers, label: 'Secondary stop')); } for (final tag in tags.where((tag) => tag.trim().isNotEmpty).take(4)) { items.add(_MetaItem(icon: Icons.local_offer, label: tag)); } if (websiteLabel != null && websiteLabel.isNotEmpty && onOpenWebsite != null) { items.add(_MetaItem(icon: Icons.link, label: 'Website', onTap: onOpenWebsite)); } return items; } } class _LandmarkMedia extends StatelessWidget { const _LandmarkMedia({required this.landmark, required this.position, required this.type}); final Landmark landmark; final int position; final LandmarkTypeEnum type; @override Widget build(BuildContext context) { const width = 116.0; const mediaHeight = 140.0; final label = _typeLabel(type); final icon = _typeIcon(type); return SizedBox( width: width, child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, mainAxisSize: MainAxisSize.min, children: [ SizedBox( height: mediaHeight, child: Stack( children: [ Positioned.fill(child: _buildMedia()), Positioned( left: 8, bottom: 8, child: DecoratedBox( decoration: BoxDecoration(color: Colors.black.withValues(alpha: 0.65), borderRadius: BorderRadius.circular(18)), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon(icon, size: 14, color: Colors.white), const SizedBox(width: 4), Text(label, style: const TextStyle(color: Colors.white, fontSize: 12)), ], ), ), ), ), ], ), ), Container( color: Colors.black.withValues(alpha: 0.6), padding: const EdgeInsets.symmetric(vertical: 5), child: Center( child: Text( '#$position', style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold), ), ), ), ], ), ); } Widget _buildMedia() { if (landmark.imageUrl != null && landmark.imageUrl!.isNotEmpty) { return CachedNetworkImage( imageUrl: landmark.imageUrl!, fit: BoxFit.cover, placeholder: (context, url) => const Center(child: CircularProgressIndicator(strokeWidth: 2)), errorWidget: (context, url, error) => _placeholder(), ); } return _placeholder(); } Widget _placeholder() { return Container( decoration: const BoxDecoration( gradient: LinearGradient(begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [Color(0xFFF9B208), Color(0xFFE72E77)]), ), child: const Center(child: Icon(Icons.photo, color: Colors.white, size: 32)), ); } String _typeLabel(LandmarkTypeEnum type) { switch (type) { case LandmarkTypeEnum.start: return 'Start'; case LandmarkTypeEnum.finish: return 'Finish'; case LandmarkTypeEnum.shopping: return 'Shopping'; case LandmarkTypeEnum.nature: return 'Nature'; case LandmarkTypeEnum.sightseeing: return 'Sight'; } } IconData _typeIcon(LandmarkTypeEnum type) { switch (type) { case LandmarkTypeEnum.start: return Icons.flag; case LandmarkTypeEnum.finish: return Icons.flag_circle; case LandmarkTypeEnum.shopping: return Icons.shopping_bag; case LandmarkTypeEnum.nature: return Icons.park; case LandmarkTypeEnum.sightseeing: return Icons.place; } } } class _MetaScroller extends StatelessWidget { const _MetaScroller({required this.items}); final List<_MetaItem> items; @override Widget build(BuildContext context) { return SizedBox( height: 34, child: ListView.separated( scrollDirection: Axis.horizontal, physics: const BouncingScrollPhysics(), itemCount: items.length, separatorBuilder: (_, __) => const SizedBox(width: 8), itemBuilder: (context, index) { final item = items[index]; final chip = Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), decoration: BoxDecoration( color: Colors.grey.shade200, borderRadius: BorderRadius.circular(14), border: Border.all(color: Colors.grey.shade300), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon(item.icon, size: 14, color: Colors.grey.shade800), const SizedBox(width: 4), Text(item.label, style: const TextStyle(fontSize: 12)), ], ), ); if (item.onTap == null) return chip; return Material( color: Colors.transparent, child: InkWell(borderRadius: BorderRadius.circular(14), onTap: item.onTap, child: chip), ); }, ), ); } } class _MetaItem { const _MetaItem({required this.icon, required this.label, this.onTap}); final IconData icon; final String label; final VoidCallback? onTap; }