import 'package:commons_core/commons_core.dart'; import 'package:flutter/material.dart'; import '../i18n/strings.g.dart'; import 'theme.dart'; /// Human words for an offer's reciprocity mode. Shared by the market list and /// the offer detail page so both read the same way. String offerTypeLabel(Translations t, OfferType type) => switch (type) { OfferType.gift => t.share.gift, OfferType.exchange => t.share.exchange, OfferType.sale => t.share.sell, OfferType.wanted => t.market.wanted, OfferType.lend => t.share.exchange, }; /// A small square thumbnail of an offer's photo for the market list card. /// Remote (published) image, so it degrades gracefully: a spinner while it /// loads and a neutral placeholder when the media server can't be reached. class OfferThumbnail extends StatelessWidget { const OfferThumbnail({ required this.url, required this.semanticLabel, this.size = 56, super.key, }); final String url; final String semanticLabel; final double size; @override Widget build(BuildContext context) { return ClipRRect( borderRadius: BorderRadius.circular(10), child: Image.network( url, width: size, height: size, fit: BoxFit.cover, semanticLabel: semanticLabel, loadingBuilder: (context, child, progress) => progress == null ? child : _OfferImagePlaceholder(size: size, loading: true), errorBuilder: (context, _, _) => _OfferImagePlaceholder(size: size), ), ); } } /// Full-width "hero" image atop the offer detail page. Same graceful /// degradation as [OfferThumbnail]; hidden entirely when the offer has no photo. class OfferHeroImage extends StatelessWidget { const OfferHeroImage({ required this.url, required this.semanticLabel, super.key, }); final String url; final String semanticLabel; @override Widget build(BuildContext context) { return ClipRRect( borderRadius: BorderRadius.circular(14), child: AspectRatio( aspectRatio: 4 / 3, child: Image.network( url, fit: BoxFit.cover, semanticLabel: semanticLabel, loadingBuilder: (context, child, progress) => progress == null ? child : const _OfferImagePlaceholder(loading: true), errorBuilder: (context, _, _) => const _OfferImagePlaceholder(), ), ), ); } } /// Neutral fill shown while a remote offer image loads or when it can't be /// fetched — a marketplace is useless if a broken image blocks the card. class _OfferImagePlaceholder extends StatelessWidget { const _OfferImagePlaceholder({this.size, this.loading = false}); final double? size; final bool loading; @override Widget build(BuildContext context) { return Container( width: size, height: size, color: seedPrimaryContainer.withValues(alpha: 0.4), alignment: Alignment.center, child: loading ? const SizedBox( width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2), ) : const Icon(Icons.image_not_supported_outlined, color: seedMuted), ); } } /// The small pill showing an offer's reciprocity mode (give/swap/sell/wanted). class OfferTypeChip extends StatelessWidget { const OfferTypeChip({required this.label, super.key}); final String label; @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), decoration: BoxDecoration( color: seedPrimaryContainer, borderRadius: BorderRadius.circular(20), ), child: Text( label, style: const TextStyle( color: seedOnPrimaryContainer, fontSize: 12, fontWeight: FontWeight.w600, ), ), ); } }