// ================================================================ // lib/screens/info_screen.dart // Pantalla de Información Relevante sobre Manejo de Residuos // ================================================================ // // PROPÓSITO: // Educar al usuario sobre separación, reciclaje y manejo // correcto de residuos. Contenido cargado desde el backend // con fallback local si no hay conexión. // // FLUJO: // 1. Lista de tarjetas por categoría (vista principal) // 2. Tap en tarjeta → detalle del artículo // 3. Cada artículo tiene secciones + consejo rápido destacado // // NAVEGACIÓN: // Agregar en main.dart: // '/info': (context) => const InfoScreen(), // Y en home_screen.dart el botón: // Navigator.pushNamed(context, '/info') // ================================================================ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:http/http.dart' as http; // ---------------------------------------------------------------- // MODELOS // ---------------------------------------------------------------- class Subseccion { final String subtitulo; final String texto; Subseccion({required this.subtitulo, required this.texto}); factory Subseccion.fromJson(Map json) => Subseccion(subtitulo: json['subtitulo'], texto: json['texto']); } class Articulo { final String id; final String categoria; final String emoji; final String titulo; final String resumen; final List contenido; final String consejoRapido; Articulo({ required this.id, required this.categoria, required this.emoji, required this.titulo, required this.resumen, required this.contenido, required this.consejoRapido, }); factory Articulo.fromJson(Map json) => Articulo( id: json['id'], categoria: json['categoria'], emoji: json['emoji'], titulo: json['titulo'], resumen: json['resumen'], contenido: (json['contenido'] as List) .map((s) => Subseccion.fromJson(s)) .toList(), consejoRapido: json['consejo_rapido'], ); } // ---------------------------------------------------------------- // DATOS LOCALES DE FALLBACK // Se usan si el backend no está disponible. // ---------------------------------------------------------------- const _articulosFallback = [ { "id": "separacion-basica", "categoria": "Separación", "emoji": "♻️", "titulo": "Cómo separar correctamente tu basura", "resumen": "La separación correcta es el primer paso para reciclar y reducir el impacto ambiental.", "contenido": [ {"subtitulo": "Residuos Orgánicos 🟤", "texto": "Restos de comida, cáscaras, posos de café. Bolsa oscura. Se convierten en composta."}, {"subtitulo": "Inorgánicos Reciclables 🟡", "texto": "PET, cartón limpio, vidrio, latas. Bolsa transparente. Deben estar limpios y secos."}, {"subtitulo": "No Reciclables 🔴", "texto": "Papel higiénico usado, pañales, colillas. Bolsa negra."}, {"subtitulo": "Residuos Especiales ⚠️", "texto": "Pilas, medicamentos, electrónicos. NUNCA con basura regular. Lleva a puntos de acopio."}, ], "consejo_rapido": "Si vino de la naturaleza y se pudre → orgánico. Si es artificial y limpio → reciclable.", }, { "id": "cuando-sacar", "categoria": "Horarios", "emoji": "⏰", "titulo": "¿Cuándo sacar tu basura?", "resumen": "Sacar la basura en el momento correcto evita plagas, malos olores y que el camión se la pierda.", "contenido": [ {"subtitulo": "El momento ideal", "texto": "Saca cuando recibas la alerta de 'Camión Cercano' en la app. El camión está a menos de 15 minutos."}, {"subtitulo": "¿Por qué no de noche?", "texto": "Atrae fauna que rompe bolsas y dispersa residuos. El plástico se deteriora con la humedad."}, {"subtitulo": "¿Si me lo pierdo?", "texto": "Guarda la basura hasta el siguiente día. Nunca dejes bolsas en la vía pública fuera del horario."}, ], "consejo_rapido": "Espera la alerta de la app antes de salir con tus bolsas.", }, { "id": "plasticos-guia", "categoria": "Reciclaje", "emoji": "🧴", "titulo": "Guía de plásticos: cuáles sí y cuáles no", "resumen": "No todos los plásticos son iguales. Aprende a leer el número en el triángulo de reciclaje.", "contenido": [ {"subtitulo": "✅ #1 PET", "texto": "Botellas de agua y refresco. El más reciclado. Aplástalo y quita la tapa."}, {"subtitulo": "✅ #2 HDPE", "texto": "Garrafones, botellas de leche, shampú. Enjuágalo antes."}, {"subtitulo": "✅ #5 PP", "texto": "Tapas, envases de yogur. Sí se recicla pero menos centros lo aceptan."}, {"subtitulo": "❌ #3, #6, #7", "texto": "PVC, unicel, policarbonato. Difíciles de reciclar. Van a basura general."}, {"subtitulo": "❌ Bolsas de plástico", "texto": "No al reciclaje de casa. Lleva a centros de acopio en supermercados."}, ], "consejo_rapido": "Busca el número en el triángulo en el fondo del envase. #1 y #2 siempre al reciclaje.", }, { "id": "residuos-peligrosos", "categoria": "Residuos Especiales", "emoji": "⚠️", "titulo": "Residuos peligrosos: cómo deshacerte de ellos", "resumen": "Pilas, medicamentos y electrónicos requieren manejo especial para no contaminar.", "contenido": [ {"subtitulo": "Pilas y baterías", "texto": "Una pila AA puede contaminar 600,000 litros de agua. Lleva a Walmart, Soriana o OXXO."}, {"subtitulo": "Medicamentos caducados", "texto": "No al drenaje. Farmacias del Ahorro y Benavides tienen contenedores REPARED."}, {"subtitulo": "Electrónicos (RAEE)", "texto": "Celulares, cables, focos LED. Contienen plomo y mercurio. Lleva a Best Buy o Liverpool."}, {"subtitulo": "Aceite de cocina", "texto": "Un litro contamina 1,000 litros de agua. Guárdalo en botella PET y lleva a acopio."}, ], "consejo_rapido": "Guarda una caja en casa solo para residuos peligrosos. Cuando esté llena, busca el punto de acopio.", }, { "id": "composta", "categoria": "Compostaje", "emoji": "🌱", "titulo": "Haz composta en casa", "resumen": "Convierte tus residuos orgánicos en abono natural. Es más fácil de lo que crees.", "contenido": [ {"subtitulo": "¿Qué necesitas?", "texto": "Un contenedor con tapa, residuos orgánicos, tierra o hojarasca y paciencia."}, {"subtitulo": "¿Qué puedes compostar?", "texto": "Cáscaras de frutas, restos sin carne, posos de café, cáscaras de huevo, hojas secas."}, {"subtitulo": "¿Qué NO?", "texto": "Carnes, lácteos, aceites (atraen plagas), excrementos de mascotas, plásticos."}, {"subtitulo": "El proceso", "texto": "Alterna capas húmedas con secas. Voltea cada semana. En 2-3 meses tienes composta lista."}, ], "consejo_rapido": "La composta lista huele a tierra mojada, no a podrido. Si huele mal, agrega material seco.", }, { "id": "impacto-ambiental", "categoria": "Medio Ambiente", "emoji": "🌍", "titulo": "El impacto real de reciclar", "resumen": "Números concretos para entender por qué vale la pena separar tu basura cada día.", "contenido": [ {"subtitulo": "Papel y cartón", "texto": "1 tonelada reciclada salva 17 árboles y ahorra 26,000 litros de agua."}, {"subtitulo": "Aluminio", "texto": "Reciclar una lata ahorra energía para un foco LED por 20 horas. Se recicla infinitas veces."}, {"subtitulo": "Vidrio", "texto": "Tarda 4,000 años en degradarse. Reciclarlo reduce 20% las emisiones de su producción."}, {"subtitulo": "Residuos en México", "texto": "México genera 120,000 toneladas de basura al día. Solo el 9% se recicla. Podemos hacer más."}, ], "consejo_rapido": "Cada lata de aluminio reciclada ahorra energía equivalente a medio litro de gasolina. Sí importa.", }, ]; // ---------------------------------------------------------------- // COLORES POR CATEGORÍA // ---------------------------------------------------------------- Color _colorCategoria(String categoria) { switch (categoria) { case 'Separación': return const Color(0xFF2E7D32); case 'Horarios': return const Color(0xFF1565C0); case 'Reciclaje': return const Color(0xFF00838F); case 'Compostaje': return const Color(0xFF558B2F); case 'Residuos Especiales': return const Color(0xFFE65100); case 'Medio Ambiente': return const Color(0xFF4527A0); default: return const Color(0xFF37474F); } } // ================================================================ // PANTALLA PRINCIPAL: Lista de artículos // ================================================================ class InfoScreen extends StatefulWidget { const InfoScreen({super.key}); @override State createState() => _InfoScreenState(); } class _InfoScreenState extends State { List _articulos = []; bool _cargando = true; String? _categoriaSeleccionada; static const String _baseUrl = 'http://192.168.198.224:8000'; @override void initState() { super.initState(); _cargarArticulos(); } Future _cargarArticulos() async { try { final response = await http .get(Uri.parse('$_baseUrl/api/info')) .timeout(const Duration(seconds: 8)); if (response.statusCode == 200) { final data = json.decode(response.body); // El endpoint de lista no trae contenido completo, // así que cargamos desde fallback y enriquecemos con backend final ids = (data['articulos'] as List).map((a) => a['id'] as String).toList(); final articulos = []; for (final fallback in _articulosFallback) { if (ids.contains(fallback['id'])) { articulos.add(Articulo.fromJson(fallback as Map)); } } if (mounted) setState(() { _articulos = articulos; _cargando = false; }); return; } } catch (_) { // Fallback silencioso } // Carga local si el backend no responde if (mounted) { setState(() { _articulos = _articulosFallback .map((a) => Articulo.fromJson(a as Map)) .toList(); _cargando = false; }); } } List get _categorias => _articulos.map((a) => a.categoria).toSet().toList(); List get _articulosFiltrados => _categoriaSeleccionada == null ? _articulos : _articulos.where((a) => a.categoria == _categoriaSeleccionada).toList(); @override Widget build(BuildContext context) { return Scaffold( backgroundColor: const Color(0xFFF5F7F5), appBar: AppBar( title: const Text('Información relevante'), backgroundColor: const Color(0xFF2E7D32), foregroundColor: Colors.white, elevation: 0, ), body: _cargando ? const Center(child: CircularProgressIndicator()) : Column( children: [ // ── HEADER VERDE ───────────────────────────────── Container( width: double.infinity, color: const Color(0xFF2E7D32), padding: const EdgeInsets.fromLTRB(20, 0, 20, 20), child: const Text( 'Aprende a manejar tus residuos de forma responsable y reduce tu impacto ambiental.', style: TextStyle(color: Colors.white70, fontSize: 14), ), ), // ── FILTROS POR CATEGORÍA ───────────────────────── if (_categorias.isNotEmpty) SizedBox( height: 48, child: ListView( scrollDirection: Axis.horizontal, padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), children: [ _FiltroChip( label: 'Todos', seleccionado: _categoriaSeleccionada == null, color: const Color(0xFF2E7D32), onTap: () => setState(() => _categoriaSeleccionada = null), ), ...(_categorias.map((cat) => _FiltroChip( label: cat, seleccionado: _categoriaSeleccionada == cat, color: _colorCategoria(cat), onTap: () => setState(() => _categoriaSeleccionada = cat), ))), ], ), ), // ── LISTA DE ARTÍCULOS ──────────────────────────── Expanded( child: ListView.builder( padding: const EdgeInsets.fromLTRB(16, 8, 16, 24), itemCount: _articulosFiltrados.length, itemBuilder: (context, index) { final articulo = _articulosFiltrados[index]; return _TarjetaArticulo( articulo: articulo, onTap: () => Navigator.push( context, MaterialPageRoute( builder: (_) => _DetalleArticuloScreen(articulo: articulo), ), ), ); }, ), ), ], ), ); } } // ================================================================ // WIDGET: Chip de filtro por categoría // ================================================================ class _FiltroChip extends StatelessWidget { final String label; final bool seleccionado; final Color color; final VoidCallback onTap; const _FiltroChip({ required this.label, required this.seleccionado, required this.color, required this.onTap, }); @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.only(right: 8), child: GestureDetector( onTap: onTap, child: AnimatedContainer( duration: const Duration(milliseconds: 200), padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 6), decoration: BoxDecoration( color: seleccionado ? color : Colors.white, borderRadius: BorderRadius.circular(20), border: Border.all(color: color, width: 1.5), ), child: Text( label, style: TextStyle( color: seleccionado ? Colors.white : color, fontWeight: FontWeight.w600, fontSize: 13, ), ), ), ), ); } } // ================================================================ // WIDGET: Tarjeta de artículo en la lista // ================================================================ class _TarjetaArticulo extends StatelessWidget { final Articulo articulo; final VoidCallback onTap; const _TarjetaArticulo({required this.articulo, required this.onTap}); @override Widget build(BuildContext context) { final color = _colorCategoria(articulo.categoria); return Padding( padding: const EdgeInsets.only(bottom: 12), child: GestureDetector( onTap: onTap, child: Container( decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.06), blurRadius: 8, offset: const Offset(0, 2), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Barra de color superior Container( height: 6, decoration: BoxDecoration( color: color, borderRadius: const BorderRadius.vertical(top: Radius.circular(16)), ), ), Padding( padding: const EdgeInsets.all(16), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Emoji grande Container( width: 52, height: 52, decoration: BoxDecoration( color: color.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(12), ), child: Center( child: Text(articulo.emoji, style: const TextStyle(fontSize: 28)), ), ), const SizedBox(width: 14), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Badge de categoría Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), decoration: BoxDecoration( color: color.withValues(alpha: 0.12), borderRadius: BorderRadius.circular(8), ), child: Text( articulo.categoria, style: TextStyle( color: color, fontSize: 11, fontWeight: FontWeight.w700, letterSpacing: 0.5, ), ), ), const SizedBox(height: 6), Text( articulo.titulo, style: const TextStyle( fontSize: 15, fontWeight: FontWeight.bold, color: Color(0xFF1A1A1A), ), ), const SizedBox(height: 4), Text( articulo.resumen, style: TextStyle( fontSize: 13, color: Colors.grey.shade600, height: 1.4, ), maxLines: 2, overflow: TextOverflow.ellipsis, ), ], ), ), Icon(Icons.arrow_forward_ios, size: 14, color: Colors.grey.shade400), ], ), ), // Consejo rápido al pie Container( width: double.infinity, margin: const EdgeInsets.fromLTRB(16, 0, 16, 16), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), decoration: BoxDecoration( color: color.withValues(alpha: 0.07), borderRadius: BorderRadius.circular(10), ), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('💡', style: TextStyle(fontSize: 14, color: color)), const SizedBox(width: 8), Expanded( child: Text( articulo.consejoRapido, style: TextStyle( fontSize: 12, color: color, fontWeight: FontWeight.w500, height: 1.4, ), ), ), ], ), ), ], ), ), ), ); } } // ================================================================ // PANTALLA DE DETALLE DE ARTÍCULO // ================================================================ class _DetalleArticuloScreen extends StatelessWidget { final Articulo articulo; const _DetalleArticuloScreen({required this.articulo}); @override Widget build(BuildContext context) { final color = _colorCategoria(articulo.categoria); return Scaffold( backgroundColor: const Color(0xFFF5F7F5), body: CustomScrollView( slivers: [ // ── APP BAR CON COLOR DE CATEGORÍA ─────────────────── SliverAppBar( expandedHeight: 160, pinned: true, backgroundColor: color, foregroundColor: Colors.white, flexibleSpace: FlexibleSpaceBar( title: Text( articulo.titulo, style: const TextStyle( color: Colors.white, fontSize: 15, fontWeight: FontWeight.bold, ), ), background: Container( decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [color, color.withValues(alpha: 0.7)], ), ), child: Center( child: Text(articulo.emoji, style: const TextStyle(fontSize: 64)), ), ), ), ), SliverToBoxAdapter( child: Padding( padding: const EdgeInsets.all(20), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Badge categoría Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 5), decoration: BoxDecoration( color: color.withValues(alpha: 0.12), borderRadius: BorderRadius.circular(20), ), child: Text( articulo.categoria, style: TextStyle(color: color, fontWeight: FontWeight.w700, fontSize: 13), ), ), const SizedBox(height: 12), // Resumen Text( articulo.resumen, style: const TextStyle( fontSize: 16, color: Color(0xFF333333), height: 1.5, ), ), const SizedBox(height: 24), // ── SECCIONES DE CONTENIDO ─────────────────── ...articulo.contenido.map((seccion) => _SeccionCard( seccion: seccion, color: color, )), const SizedBox(height: 8), // ── CONSEJO RÁPIDO DESTACADO ───────────────── Container( width: double.infinity, padding: const EdgeInsets.all(20), decoration: BoxDecoration( color: color, borderRadius: BorderRadius.circular(16), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Row( children: [ Text('💡', style: TextStyle(fontSize: 20)), SizedBox(width: 8), Text( 'Consejo rápido', style: TextStyle( color: Colors.white, fontWeight: FontWeight.bold, fontSize: 16, ), ), ], ), const SizedBox(height: 10), Text( articulo.consejoRapido, style: const TextStyle( color: Colors.white, fontSize: 15, height: 1.5, ), ), ], ), ), const SizedBox(height: 32), ], ), ), ), ], ), ); } } // ================================================================ // WIDGET: Tarjeta de una sección dentro del detalle // ================================================================ class _SeccionCard extends StatelessWidget { final Subseccion seccion; final Color color; const _SeccionCard({required this.seccion, required this.color}); @override Widget build(BuildContext context) { return Container( width: double.infinity, margin: const EdgeInsets.only(bottom: 12), padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12), border: Border(left: BorderSide(color: color, width: 4)), boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.04), blurRadius: 6, offset: const Offset(0, 2), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( seccion.subtitulo, style: TextStyle( fontSize: 14, fontWeight: FontWeight.bold, color: color, ), ), const SizedBox(height: 6), Text( seccion.texto, style: const TextStyle( fontSize: 14, color: Color(0xFF444444), height: 1.5, ), ), ], ), ); } }