diff --git a/apps/app_seeds/lib/app.dart b/apps/app_seeds/lib/app.dart index 1579674..665cfd7 100644 --- a/apps/app_seeds/lib/app.dart +++ b/apps/app_seeds/lib/app.dart @@ -8,13 +8,13 @@ import 'data/variety_repository.dart'; import 'i18n/strings.g.dart'; import 'state/inventory_cubit.dart'; import 'state/variety_detail_cubit.dart'; +import 'ui/home_screen.dart'; import 'ui/inventory_list_screen.dart'; import 'ui/theme.dart'; import 'ui/variety_detail_screen.dart'; -/// Root widget. Provides the repository + inventory cubit to the tree and wires -/// go_router. The list is `/`; `/variety/:id` is a placeholder detail route -/// (the full item screen is a follow-on story). +/// Root widget. Provides the repositories to the tree and wires go_router: +/// `/` is the home menu, `/inventory` the list, `/variety/:id` the item detail. class TaneApp extends StatelessWidget { TaneApp({required this.repository, required this.species, super.key}) : _router = _buildRouter(repository); @@ -26,8 +26,9 @@ class TaneApp extends StatelessWidget { static GoRouter _buildRouter(VarietyRepository repository) { return GoRouter( routes: [ + GoRoute(path: '/', builder: (context, state) => const HomeScreen()), GoRoute( - path: '/', + path: '/inventory', builder: (context, state) => BlocProvider( create: (_) => InventoryCubit(repository), child: const InventoryListScreen(), diff --git a/apps/app_seeds/lib/i18n/en.i18n.json b/apps/app_seeds/lib/i18n/en.i18n.json index b5cb8f8..2e0c0fa 100644 --- a/apps/app_seeds/lib/i18n/en.i18n.json +++ b/apps/app_seeds/lib/i18n/en.i18n.json @@ -7,7 +7,21 @@ "cancel": "Cancel", "delete": "Delete", "edit": "Edit", - "type": "Type" + "type": "Type", + "comingSoon": "Coming soon" + }, + "home": { + "openMarket": "Open market", + "yourInventory": "Your inventory" + }, + "menu": { + "inventory": "Inventory", + "market": "Market", + "profile": "Your profile", + "chat": "Chat", + "wishlist": "Wishlist", + "following": "Following", + "settings": "Settings" }, "inventory": { "title": "Inventory", diff --git a/apps/app_seeds/lib/i18n/es.i18n.json b/apps/app_seeds/lib/i18n/es.i18n.json index 3254e46..c5e93e4 100644 --- a/apps/app_seeds/lib/i18n/es.i18n.json +++ b/apps/app_seeds/lib/i18n/es.i18n.json @@ -7,7 +7,21 @@ "cancel": "Cancelar", "delete": "Eliminar", "edit": "Editar", - "type": "Tipo" + "type": "Tipo", + "comingSoon": "Pronto" + }, + "home": { + "openMarket": "Mercado abierto", + "yourInventory": "Tu inventario" + }, + "menu": { + "inventory": "Inventario", + "market": "Mercado", + "profile": "Tu perfil", + "chat": "Chat", + "wishlist": "Lista de deseos", + "following": "Siguiendo", + "settings": "Ajustes" }, "inventory": { "title": "Inventario", diff --git a/apps/app_seeds/lib/i18n/strings.g.dart b/apps/app_seeds/lib/i18n/strings.g.dart index af449a3..296d59d 100644 --- a/apps/app_seeds/lib/i18n/strings.g.dart +++ b/apps/app_seeds/lib/i18n/strings.g.dart @@ -4,9 +4,9 @@ /// To regenerate, run: `dart run slang` /// /// Locales: 2 -/// Strings: 216 (108 per locale) +/// Strings: 236 (118 per locale) /// -/// Built on 2026-07-08 at 10:02 UTC +/// Built on 2026-07-08 at 10:18 UTC // coverage:ignore-file // ignore_for_file: type=lint, unused_import diff --git a/apps/app_seeds/lib/i18n/strings_en.g.dart b/apps/app_seeds/lib/i18n/strings_en.g.dart index 1baccc0..439bc69 100644 --- a/apps/app_seeds/lib/i18n/strings_en.g.dart +++ b/apps/app_seeds/lib/i18n/strings_en.g.dart @@ -42,6 +42,8 @@ class Translations with BaseTranslations { // Translations late final Translations$app$en app = Translations$app$en.internal(_root); late final Translations$common$en common = Translations$common$en.internal(_root); + late final Translations$home$en home = Translations$home$en.internal(_root); + late final Translations$menu$en menu = Translations$menu$en.internal(_root); late final Translations$inventory$en inventory = Translations$inventory$en.internal(_root); late final Translations$quickAdd$en quickAdd = Translations$quickAdd$en.internal(_root); late final Translations$detail$en detail = Translations$detail$en.internal(_root); @@ -87,6 +89,54 @@ class Translations$common$en { /// en: 'Type' String get type => 'Type'; + + /// en: 'Coming soon' + String get comingSoon => 'Coming soon'; +} + +// Path: home +class Translations$home$en { + Translations$home$en.internal(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + + /// en: 'Open market' + String get openMarket => 'Open market'; + + /// en: 'Your inventory' + String get yourInventory => 'Your inventory'; +} + +// Path: menu +class Translations$menu$en { + Translations$menu$en.internal(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + + /// en: 'Inventory' + String get inventory => 'Inventory'; + + /// en: 'Market' + String get market => 'Market'; + + /// en: 'Your profile' + String get profile => 'Your profile'; + + /// en: 'Chat' + String get chat => 'Chat'; + + /// en: 'Wishlist' + String get wishlist => 'Wishlist'; + + /// en: 'Following' + String get following => 'Following'; + + /// en: 'Settings' + String get settings => 'Settings'; } // Path: inventory @@ -680,6 +730,16 @@ extension on Translations { 'common.delete' => 'Delete', 'common.edit' => 'Edit', 'common.type' => 'Type', + 'common.comingSoon' => 'Coming soon', + 'home.openMarket' => 'Open market', + 'home.yourInventory' => 'Your inventory', + 'menu.inventory' => 'Inventory', + 'menu.market' => 'Market', + 'menu.profile' => 'Your profile', + 'menu.chat' => 'Chat', + 'menu.wishlist' => 'Wishlist', + 'menu.following' => 'Following', + 'menu.settings' => 'Settings', 'inventory.title' => 'Inventory', 'inventory.searchHint' => 'Search seeds', 'inventory.empty' => 'No seeds yet. Tap + to add your first.', diff --git a/apps/app_seeds/lib/i18n/strings_es.g.dart b/apps/app_seeds/lib/i18n/strings_es.g.dart index a2a64c2..d778202 100644 --- a/apps/app_seeds/lib/i18n/strings_es.g.dart +++ b/apps/app_seeds/lib/i18n/strings_es.g.dart @@ -41,6 +41,8 @@ class TranslationsEs extends Translations with BaseTranslations 'Eliminar'; @override String get edit => 'Editar'; @override String get type => 'Tipo'; + @override String get comingSoon => 'Pronto'; +} + +// Path: home +class _Translations$home$es extends Translations$home$en { + _Translations$home$es._(TranslationsEs root) : this._root = root, super.internal(root); + + final TranslationsEs _root; // ignore: unused_field + + // Translations + @override String get openMarket => 'Mercado abierto'; + @override String get yourInventory => 'Tu inventario'; +} + +// Path: menu +class _Translations$menu$es extends Translations$menu$en { + _Translations$menu$es._(TranslationsEs root) : this._root = root, super.internal(root); + + final TranslationsEs _root; // ignore: unused_field + + // Translations + @override String get inventory => 'Inventario'; + @override String get market => 'Mercado'; + @override String get profile => 'Tu perfil'; + @override String get chat => 'Chat'; + @override String get wishlist => 'Lista de deseos'; + @override String get following => 'Siguiendo'; + @override String get settings => 'Ajustes'; } // Path: inventory @@ -485,6 +515,16 @@ extension on TranslationsEs { 'common.delete' => 'Eliminar', 'common.edit' => 'Editar', 'common.type' => 'Tipo', + 'common.comingSoon' => 'Pronto', + 'home.openMarket' => 'Mercado abierto', + 'home.yourInventory' => 'Tu inventario', + 'menu.inventory' => 'Inventario', + 'menu.market' => 'Mercado', + 'menu.profile' => 'Tu perfil', + 'menu.chat' => 'Chat', + 'menu.wishlist' => 'Lista de deseos', + 'menu.following' => 'Siguiendo', + 'menu.settings' => 'Ajustes', 'inventory.title' => 'Inventario', 'inventory.searchHint' => 'Buscar semillas', 'inventory.empty' => 'Aún no hay semillas. Toca + para añadir la primera.', diff --git a/apps/app_seeds/lib/ui/app_drawer.dart b/apps/app_seeds/lib/ui/app_drawer.dart new file mode 100644 index 0000000..72aaa2e --- /dev/null +++ b/apps/app_seeds/lib/ui/app_drawer.dart @@ -0,0 +1,115 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:material_symbols_icons/symbols.dart'; + +import '../i18n/strings.g.dart'; +import 'seed_glyph.dart'; +import 'theme.dart'; + +/// The app's navigation drawer (mockup 03). Inventory is live; the social items +/// (market, profile, chat…) belong to Block 2 and are shown disabled with a +/// "coming soon" tag so the roadmap is visible. +class AppDrawer extends StatelessWidget { + const AppDrawer({super.key}); + + @override + Widget build(BuildContext context) { + final t = context.t; + return Drawer( + backgroundColor: Colors.white, + child: SafeArea( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const _DrawerHeader(), + _ActiveItem( + icon: const SeedGlyph(SeedGlyphs.jars, size: 22), + label: t.menu.inventory, + onTap: () { + Navigator.of(context).pop(); + context.go('/inventory'); + }, + ), + _SoonItem(icon: Symbols.storefront, label: t.menu.market), + const Divider(), + _SoonItem(icon: Icons.person_outline, label: t.menu.profile), + _SoonItem(icon: Icons.chat_bubble_outline, label: t.menu.chat), + _SoonItem(icon: Icons.favorite_border, label: t.menu.wishlist), + _SoonItem(icon: Icons.group_outlined, label: t.menu.following), + const Spacer(), + const Divider(), + _SoonItem(icon: Icons.settings_outlined, label: t.menu.settings), + ], + ), + ), + ); + } +} + +class _DrawerHeader extends StatelessWidget { + const _DrawerHeader(); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.fromLTRB(16, 12, 16, 12), + child: Row( + children: [ + const Icon(Symbols.eco, color: seedGreen, size: 32), + const SizedBox(width: 12), + Text( + context.t.app.title, + style: Theme.of(context).textTheme.titleLarge?.copyWith( + color: seedGreenDark, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ); + } +} + +class _ActiveItem extends StatelessWidget { + const _ActiveItem({ + required this.icon, + required this.label, + required this.onTap, + }); + + final Widget icon; + final String label; + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + return ListTile( + leading: SizedBox(width: 24, child: Center(child: icon)), + title: Text(label), + onTap: onTap, + ); + } +} + +/// A disabled Block-2 destination with a subtle "coming soon" tag. +class _SoonItem extends StatelessWidget { + const _SoonItem({required this.icon, required this.label}); + + final IconData icon; + final String label; + + @override + Widget build(BuildContext context) { + return ListTile( + enabled: false, + leading: Icon(icon), + title: Text(label), + trailing: Text( + context.t.common.comingSoon, + style: Theme.of( + context, + ).textTheme.labelSmall?.copyWith(color: Colors.black38), + ), + ); + } +} diff --git a/apps/app_seeds/lib/ui/home_screen.dart b/apps/app_seeds/lib/ui/home_screen.dart new file mode 100644 index 0000000..be2b9e7 --- /dev/null +++ b/apps/app_seeds/lib/ui/home_screen.dart @@ -0,0 +1,159 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:material_symbols_icons/symbols.dart'; + +import '../i18n/strings.g.dart'; +import 'app_drawer.dart'; +import 'seed_glyph.dart'; +import 'theme.dart'; + +/// The main menu (mockup 02): a sprout logo over a faint seed-glyph watermark, +/// with the big "open market" (Block 2, coming soon) and "your inventory" +/// destinations. The hamburger opens [AppDrawer]. +class HomeScreen extends StatelessWidget { + const HomeScreen({super.key}); + + @override + Widget build(BuildContext context) { + final t = context.t; + return Scaffold( + appBar: AppBar(title: Text(t.app.title)), + drawer: const AppDrawer(), + body: Stack( + children: [ + const Positioned.fill(child: _SeedWatermark()), + Center( + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 440), + child: Padding( + padding: const EdgeInsets.all(24), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const Icon( + Symbols.eco, + size: 120, + color: seedGreen, + fill: 1, + ), + const SizedBox(height: 40), + _MenuButton( + icon: const SeedGlyph( + SeedGlyphs.share, + size: 26, + color: Colors.white, + ), + label: t.home.openMarket, + tag: t.common.comingSoon, + onTap: null, + ), + const SizedBox(height: 16), + _MenuButton( + key: const Key('home.inventory'), + icon: const Icon(Icons.list, color: Colors.white), + label: t.home.yourInventory, + onTap: () => context.go('/inventory'), + ), + ], + ), + ), + ), + ), + ], + ), + ); + } +} + +class _MenuButton extends StatelessWidget { + const _MenuButton({ + required this.icon, + required this.label, + this.onTap, + this.tag, + super.key, + }); + + final Widget icon; + final String label; + final VoidCallback? onTap; + final String? tag; + + @override + Widget build(BuildContext context) { + final enabled = onTap != null; + return Material( + color: enabled ? seedGreen : seedGreen.withValues(alpha: 0.45), + borderRadius: BorderRadius.circular(10), + child: InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(10), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 20), + child: Row( + children: [ + icon, + const SizedBox(width: 16), + Expanded( + child: Text( + label, + style: const TextStyle( + color: Colors.white, + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + ), + if (tag != null) + Text( + tag!, + style: const TextStyle(color: Colors.white70, fontSize: 12), + ), + ], + ), + ), + ), + ); + } +} + +/// A faint scatter of seed pictograms behind the menu (like mockup 02). +class _SeedWatermark extends StatelessWidget { + const _SeedWatermark(); + + static const _glyphs = [ + SeedGlyphs.jars, + SeedGlyphs.envelope, + SeedGlyphs.bigSpoon, + SeedGlyphs.jar, + SeedGlyphs.sack, + SeedGlyphs.scattered, + SeedGlyphs.mug, + SeedGlyphs.smallSpoon, + ]; + + @override + Widget build(BuildContext context) { + return IgnorePointer( + child: Opacity( + opacity: 0.06, + child: Align( + alignment: Alignment.bottomCenter, + child: Padding( + padding: const EdgeInsets.only(bottom: 24), + child: Wrap( + spacing: 28, + runSpacing: 18, + alignment: WrapAlignment.center, + children: [ + for (final g in _glyphs) + SeedGlyph(g, size: 64, color: seedGreenDark), + ], + ), + ), + ), + ), + ); + } +} diff --git a/apps/app_seeds/lib/ui/inventory_list_screen.dart b/apps/app_seeds/lib/ui/inventory_list_screen.dart index 9c2e52e..3301000 100644 --- a/apps/app_seeds/lib/ui/inventory_list_screen.dart +++ b/apps/app_seeds/lib/ui/inventory_list_screen.dart @@ -5,6 +5,7 @@ import 'package:go_router/go_router.dart'; import '../data/variety_repository.dart'; import '../i18n/strings.g.dart'; import '../state/inventory_cubit.dart'; +import 'app_drawer.dart'; import 'quick_add_sheet.dart'; import 'seed_glyph.dart'; import 'theme.dart'; @@ -19,6 +20,7 @@ class InventoryListScreen extends StatelessWidget { final t = context.t; return Scaffold( appBar: AppBar(title: Text(t.inventory.title)), + drawer: const AppDrawer(), floatingActionButton: FloatingActionButton( key: const Key('inventory.addFab'), tooltip: t.quickAdd.title, diff --git a/apps/app_seeds/test/ui/home_screen_test.dart b/apps/app_seeds/test/ui/home_screen_test.dart new file mode 100644 index 0000000..f573cc3 --- /dev/null +++ b/apps/app_seeds/test/ui/home_screen_test.dart @@ -0,0 +1,55 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:tane/app.dart'; +import 'package:tane/i18n/strings.g.dart'; + +import '../support/test_support.dart'; + +void main() { + Widget app(db) => TranslationProvider( + child: TaneApp( + repository: newTestRepository(db), + species: newTestSpeciesRepository(db), + ), + ); + + testWidgets('home shows the menu and navigates to inventory', (tester) async { + LocaleSettings.setLocaleSync(AppLocale.en); + final db = newTestDatabase(); + addTearDown(db.close); + + await tester.pumpWidget(app(db)); + await tester.pumpAndSettle(); + + expect(find.text('Your inventory'), findsOneWidget); + expect(find.text('Open market'), findsOneWidget); + expect(find.text('Coming soon'), findsOneWidget); // market is Block 2 + + await tester.tap(find.byKey(const Key('home.inventory'))); + await tester.pumpAndSettle(); + + expect(find.text('No seeds yet. Tap + to add your first.'), findsOneWidget); + await disposeTree(tester); + }); + + testWidgets('drawer opens and its Inventory item navigates', (tester) async { + LocaleSettings.setLocaleSync(AppLocale.en); + final db = newTestDatabase(); + addTearDown(db.close); + + await tester.pumpWidget(app(db)); + await tester.pumpAndSettle(); + + await tester.tap(find.byIcon(Icons.menu)); // hamburger + await tester.pumpAndSettle(); + + // Social destinations are shown but disabled. + expect(find.text('Your profile'), findsOneWidget); + + await tester.tap(find.text('Inventory')); // active drawer item + await tester.pumpAndSettle(); + + expect(find.text('No seeds yet. Tap + to add your first.'), findsOneWidget); + await disposeTree(tester); + }); +} diff --git a/apps/app_seeds/test/ui/quick_add_flow_test.dart b/apps/app_seeds/test/ui/quick_add_flow_test.dart index d0db00a..d9856fd 100644 --- a/apps/app_seeds/test/ui/quick_add_flow_test.dart +++ b/apps/app_seeds/test/ui/quick_add_flow_test.dart @@ -24,6 +24,10 @@ void main() { ); await tester.pumpAndSettle(); + // Home → inventory. + await tester.tap(find.byKey(const Key('home.inventory'))); + await tester.pumpAndSettle(); + // Starts empty. expect( find.text('No seeds yet. Tap + to add your first.'),