feat: integrate persona D (recycling guide) and routes modules

This commit is contained in:
Alan Alonso
2026-05-23 01:00:02 -06:00
parent a38ca14f38
commit 6d1845c09d
21 changed files with 2074 additions and 99 deletions

View File

@@ -0,0 +1,164 @@
// lib/features/recycling_guide/presentation/screens/recycling_guide_screen.dart
// Pantalla principal — Persona C la agrega al router así:
// GoRoute(path: '/guia', builder: (_, __) => const RecyclingGuideScreen())
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../providers/recycling_provider.dart';
import '../widgets/category_card.dart';
import '../widgets/search_result_tile.dart';
import 'category_detail_screen.dart';
class RecyclingGuideScreen extends ConsumerStatefulWidget {
const RecyclingGuideScreen({super.key});
@override
ConsumerState<RecyclingGuideScreen> createState() =>
_RecyclingGuideScreenState();
}
class _RecyclingGuideScreenState extends ConsumerState<RecyclingGuideScreen> {
final _searchCtrl = TextEditingController();
bool _buscando = false;
@override
void dispose() {
_searchCtrl.dispose();
super.dispose();
}
void _onSearchChanged(String query) {
setState(() => _buscando = query.trim().isNotEmpty);
ref.read(recyclingSearchProvider.notifier).buscar(query);
}
void _limpiarBusqueda() {
_searchCtrl.clear();
setState(() => _buscando = false);
ref.read(recyclingSearchProvider.notifier).limpiar();
FocusScope.of(context).unfocus();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Guía de separación'),
actions: [
// Badge offline — refuerza que funciona sin internet
Container(
margin: const EdgeInsets.only(right: 12),
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Colors.green.withOpacity(0.2),
borderRadius: BorderRadius.circular(12),
),
child: const Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.offline_bolt, size: 14, color: Colors.white),
SizedBox(width: 4),
Text(
'sin internet',
style: TextStyle(fontSize: 11, color: Colors.white),
),
],
),
),
],
),
body: Column(
children: [
// ── Buscador ────────────────────────────────────────────
Padding(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 8),
child: TextField(
controller: _searchCtrl,
onChanged: _onSearchChanged,
decoration: InputDecoration(
hintText: '¿Dónde va el aceite? ¿y la pila?',
prefixIcon: const Icon(Icons.search),
suffixIcon: _buscando
? IconButton(
icon: const Icon(Icons.close),
onPressed: _limpiarBusqueda,
)
: null,
),
),
),
// ── Contenido dinámico ──────────────────────────────────
Expanded(
child: _buscando
? _SearchResults()
: _CategoryList(),
),
],
),
);
}
}
// ── Lista de categorías ──────────────────────────────────────────────
class _CategoryList extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final async = ref.watch(recyclingCategoriesProvider);
return async.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (e, _) => Center(child: Text('Error: $e')),
data: (categorias) => ListView.separated(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 24),
itemCount: categorias.length,
separatorBuilder: (_, __) => const SizedBox(height: 12),
itemBuilder: (context, i) => CategoryCard(
categoria: categorias[i],
onTap: () => Navigator.push(
context,
MaterialPageRoute(
builder: (_) =>
CategoryDetailScreen(categoria: categorias[i]),
),
),
),
),
);
}
}
// ── Resultados de búsqueda ───────────────────────────────────────────
class _SearchResults extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final estado = ref.watch(recyclingSearchProvider);
return switch (estado) {
_Idle() => const SizedBox.shrink(),
_Loading() => const Center(child: CircularProgressIndicator()),
_Done(results: final r) when r.isEmpty => Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.search_off, size: 48, color: Colors.grey[400]),
const SizedBox(height: 12),
Text(
'No encontramos ese residuo.',
style: TextStyle(color: Colors.grey[600]),
),
],
),
),
_Done(results: final r) => ListView.builder(
padding: const EdgeInsets.symmetric(vertical: 8),
itemCount: r.length,
itemBuilder: (_, i) => SearchResultTile(resultado: r[i]),
),
_ => const SizedBox.shrink(),
};
}
}