feat(block2): market screen — discover seeds near you (offers UI)

Wires the 'coming soon' market card to a real discovery screen (mockups 04/05):
- MarketScreen: opens an OfferTransport lazily; local-first, so it degrades to a
  human 'set up sharing' prompt when no relay/area is set or the net is down.
- Discovers offers by the saved coarse area, lists them with human reciprocity
  labels (gift/swap/for sale), 'near you' (never exact location), price only for
  sales. Config sheet to set area + community servers (no bundled public relays).
- Threaded via TaneApp(social, socialSettings) — null keeps the card 'coming
  soon' (so Block 1 tests pass unchanged); main wires it live.
- i18n en/es/pt (market.*), reuses share.* for type labels.

3 market widget tests + home tests green; app_seeds analyzes clean.
This commit is contained in:
vjrj 2026-07-10 03:05:12 +02:00
parent 528b499209
commit c696956ee2
8 changed files with 637 additions and 16 deletions

View file

@ -11,7 +11,11 @@ import 'theme.dart';
/// to action and "Open market" (Block 2) as a disabled outlined card. The
/// hamburger opens [AppDrawer].
class HomeScreen extends StatelessWidget {
const HomeScreen({super.key});
const HomeScreen({this.marketEnabled = false, super.key});
/// When the Block 2 social layer is wired, the market becomes a live
/// destination; otherwise it stays a disabled "coming soon" card.
final bool marketEnabled;
@override
Widget build(BuildContext context) {
@ -77,10 +81,14 @@ class HomeScreen extends StatelessWidget {
),
const SizedBox(height: 16),
_OutlinedMenuCard(
key: const Key('home.market'),
icon: Icons.storefront_outlined,
label: t.home.openMarket,
subtitle: t.home.openMarketSubtitle,
tag: t.common.comingSoon,
tag: marketEnabled ? null : t.common.comingSoon,
onTap: marketEnabled
? () => context.go('/market')
: null,
),
],
),
@ -151,17 +159,24 @@ class _OutlinedMenuCard extends StatelessWidget {
required this.icon,
required this.label,
required this.subtitle,
required this.tag,
this.tag,
this.onTap,
super.key,
});
final IconData icon;
final String label;
final String subtitle;
final String tag;
/// A small trailing tag (e.g. "coming soon"); omitted for live cards.
final String? tag;
/// When set, the card is tappable; otherwise it reads as disabled.
final VoidCallback? onTap;
@override
Widget build(BuildContext context) {
return Container(
final card = Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(18),
@ -183,18 +198,31 @@ class _OutlinedMenuCard extends StatelessWidget {
subtitleColor: seedMuted,
),
),
Text(
tag.toUpperCase(),
style: const TextStyle(
color: Color(0xFFB3BDA8),
fontSize: 11,
fontWeight: FontWeight.w500,
letterSpacing: 0.5,
),
),
if (tag != null)
Text(
tag!.toUpperCase(),
style: const TextStyle(
color: Color(0xFFB3BDA8),
fontSize: 11,
fontWeight: FontWeight.w500,
letterSpacing: 0.5,
),
)
else if (onTap != null)
const Icon(Icons.chevron_right, color: seedMuted),
],
),
);
if (onTap == null) return card;
return Material(
color: Colors.transparent,
borderRadius: BorderRadius.circular(18),
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(18),
child: card,
),
);
}
}