feat: integrate persona D (recycling guide) and routes modules
This commit is contained in:
@@ -0,0 +1,215 @@
|
||||
// lib/features/recycling_guide/presentation/screens/category_detail_screen.dart
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../domain/entities/recycling_category.dart';
|
||||
|
||||
class CategoryDetailScreen extends StatelessWidget {
|
||||
final RecyclingCategory categoria;
|
||||
|
||||
const CategoryDetailScreen({super.key, required this.categoria});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final color = _parseColor(categoria.colorHex);
|
||||
|
||||
return Scaffold(
|
||||
body: CustomScrollView(
|
||||
slivers: [
|
||||
// ── Header expandible ──────────────────────────────────
|
||||
SliverAppBar(
|
||||
expandedHeight: 180,
|
||||
pinned: true,
|
||||
backgroundColor: color,
|
||||
flexibleSpace: FlexibleSpaceBar(
|
||||
title: Text(
|
||||
categoria.nombre,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
background: Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [color, color.withOpacity(0.7)],
|
||||
),
|
||||
),
|
||||
child: Center(
|
||||
child: Icon(
|
||||
_iconoDesdeNombre(categoria.icono),
|
||||
size: 80,
|
||||
color: Colors.white.withOpacity(0.3),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Descripción
|
||||
Text(
|
||||
categoria.descripcion,
|
||||
style: TextStyle(fontSize: 15, color: Colors.grey[700]),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Consejo destacado
|
||||
Container(
|
||||
padding: const EdgeInsets.all(14),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: color.withOpacity(0.3)),
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Icon(Icons.tips_and_updates, color: color, size: 20),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Text(
|
||||
categoria.consejo,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: color,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Sección: SÍ van aquí
|
||||
if (categoria.itemsAceptados.isNotEmpty) ...[
|
||||
_SectionHeader(
|
||||
label: 'Sí van aquí',
|
||||
icon: Icons.check_circle,
|
||||
color: Colors.green,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
...categoria.itemsAceptados.map(
|
||||
(item) => _ItemTile(item: item, acepta: true),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
],
|
||||
|
||||
// Sección: NO van aquí
|
||||
if (categoria.itemsRechazados.isNotEmpty) ...[
|
||||
_SectionHeader(
|
||||
label: 'No van aquí',
|
||||
icon: Icons.cancel,
|
||||
color: Colors.red,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
...categoria.itemsRechazados.map(
|
||||
(item) => _ItemTile(item: item, acepta: false),
|
||||
),
|
||||
],
|
||||
|
||||
const SizedBox(height: 32),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Color _parseColor(String hex) {
|
||||
final h = hex.replaceFirst('#', '');
|
||||
return Color(int.parse('FF$h', radix: 16));
|
||||
}
|
||||
|
||||
IconData _iconoDesdeNombre(String nombre) {
|
||||
return switch (nombre) {
|
||||
'eco' => Icons.eco,
|
||||
'recycling' => Icons.recycling,
|
||||
'masks' => Icons.masks,
|
||||
'warning_amber' => Icons.warning_amber,
|
||||
_ => Icons.category,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class _SectionHeader extends StatelessWidget {
|
||||
final String label;
|
||||
final IconData icon;
|
||||
final Color color;
|
||||
|
||||
const _SectionHeader({
|
||||
required this.label,
|
||||
required this.icon,
|
||||
required this.color,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
children: [
|
||||
Icon(icon, color: color, size: 18),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: color,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ItemTile extends StatelessWidget {
|
||||
final RecyclingItem item;
|
||||
final bool acepta;
|
||||
|
||||
const _ItemTile({required this.item, required this.acepta});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Icon(
|
||||
acepta ? Icons.check : Icons.close,
|
||||
size: 16,
|
||||
color: acepta ? Colors.green : Colors.red,
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
item.nombre,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
item.ejemplos,
|
||||
style: TextStyle(fontSize: 12, color: Colors.grey[600]),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user