From bda677df89ad7892d1e890f83f18f5261ddee5a0 Mon Sep 17 00:00:00 2001 From: Alan Alonso Date: Fri, 22 May 2026 19:13:03 -0600 Subject: [PATCH] refactor: consolidate codebase into clean architecture - Remove duplicate features structure from basura_app - Migrate recycling_guide feature to lib/ - Update main.dart to use new architecture - Clean up old views structure - Remove temporary directories (basura_app, basura_backend, wiki_hackathon) --- .claude/settings.local.json | 15 ++ .../recycling_local_datasource.dart | 46 ++++ .../repositories/recycling_repository.dart | 42 ++++ .../domain/entities/recycling_category.dart | 42 ++++ .../providers/recycling_provider.dart | 64 ++++++ .../screens/category_detail_screen.dart | 215 ++++++++++++++++++ .../screens/recycling_guide_screen.dart | 164 +++++++++++++ .../presentation/widgets/category_card.dart | 137 +++++++++++ .../widgets/search_result_tile.dart | 60 +++++ lib/main.dart | 98 +------- lib/src/views/configuracion.dart | 71 ------ lib/src/views/domicilios.dart | 87 ------- lib/src/views/horarios.dart | 148 ------------ lib/src/views/rutas.dart | 99 -------- lib/theme/app_theme.dart | 78 +++++++ 15 files changed, 868 insertions(+), 498 deletions(-) create mode 100644 .claude/settings.local.json create mode 100644 lib/features/recycling_guide/data/datasources/recycling_local_datasource.dart create mode 100644 lib/features/recycling_guide/data/repositories/recycling_repository.dart create mode 100644 lib/features/recycling_guide/domain/entities/recycling_category.dart create mode 100644 lib/features/recycling_guide/presentation/providers/recycling_provider.dart create mode 100644 lib/features/recycling_guide/presentation/screens/category_detail_screen.dart create mode 100644 lib/features/recycling_guide/presentation/screens/recycling_guide_screen.dart create mode 100644 lib/features/recycling_guide/presentation/widgets/category_card.dart create mode 100644 lib/features/recycling_guide/presentation/widgets/search_result_tile.dart delete mode 100644 lib/src/views/configuracion.dart delete mode 100644 lib/src/views/domicilios.dart delete mode 100644 lib/src/views/horarios.dart delete mode 100644 lib/src/views/rutas.dart create mode 100644 lib/theme/app_theme.dart diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..36c9de5 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,15 @@ +{ + "permissions": { + "allow": [ + "Bash(grep -E \"^d|\\\\.dart$|\\\\.py$|pubspec|requirements\")", + "Bash(rm -rf basura_app/lib/features/data)", + "Bash(rm -rf basura_app/lib/features/domain)", + "Bash(rm -rf basura_app/lib/features/presentation)", + "Bash(rm -rf basura_app/lib/core)", + "Bash(rm -rf lib/src)", + "Bash(cp -r basura_app/lib/* lib/)", + "Bash(git checkout *)", + "Bash(git add *)" + ] + } +} diff --git a/lib/features/recycling_guide/data/datasources/recycling_local_datasource.dart b/lib/features/recycling_guide/data/datasources/recycling_local_datasource.dart new file mode 100644 index 0000000..f8cdf65 --- /dev/null +++ b/lib/features/recycling_guide/data/datasources/recycling_local_datasource.dart @@ -0,0 +1,46 @@ +// lib/features/recycling_guide/data/datasources/recycling_local_datasource.dart +// Único archivo que sabe que los datos vienen de un JSON en assets. +// Funciona sin conexión a internet. + +import 'dart:convert'; +import 'package:flutter/services.dart'; +import '../../domain/entities/recycling_category.dart'; + +class RecyclingLocalDatasource { + static const _assetPath = 'assets/recycling_guide.json'; + + // Cache en memoria — se carga una sola vez durante la sesión + List? _cache; + + Future> cargarCategorias() async { + if (_cache != null) return _cache!; + + final raw = await rootBundle.loadString(_assetPath); + final List json = jsonDecode(raw); + + _cache = json.map(_mapearCategoria).toList(); + return _cache!; + } + + RecyclingCategory _mapearCategoria(dynamic json) { + final items = (json['items'] as List) + .map( + (i) => RecyclingItem( + nombre: i['nombre'] as String, + ejemplos: i['ejemplos'] as String, + acepta: i['acepta'] as bool, + ), + ) + .toList(); + + return RecyclingCategory( + id: json['id'] as String, + nombre: json['nombre'] as String, + descripcion: json['descripcion'] as String, + colorHex: json['color'] as String, + icono: json['icono'] as String, + consejo: json['consejo'] as String, + items: items, + ); + } +} diff --git a/lib/features/recycling_guide/data/repositories/recycling_repository.dart b/lib/features/recycling_guide/data/repositories/recycling_repository.dart new file mode 100644 index 0000000..73f5d46 --- /dev/null +++ b/lib/features/recycling_guide/data/repositories/recycling_repository.dart @@ -0,0 +1,42 @@ +// lib/features/recycling_guide/data/repositories/recycling_repository.dart + +import '../datasources/recycling_local_datasource.dart'; +import '../../domain/entities/recycling_category.dart'; + +class RecyclingRepository { + final RecyclingLocalDatasource _datasource; + + RecyclingRepository({RecyclingLocalDatasource? datasource}) + : _datasource = datasource ?? RecyclingLocalDatasource(); + + Future> obtenerCategorias() => + _datasource.cargarCategorias(); + + /// Busca en nombres y ejemplos de todos los items de todas las categorías. + /// Devuelve pares (categoría, item) para que la UI sepa dónde mostrar el resultado. + Future> buscar(String query) async { + if (query.trim().isEmpty) return []; + + final q = query.toLowerCase(); + final categorias = await obtenerCategorias(); + final resultados = []; + + for (final cat in categorias) { + for (final item in cat.items) { + final coincide = item.nombre.toLowerCase().contains(q) || + item.ejemplos.toLowerCase().contains(q); + if (coincide) { + resultados.add(SearchResult(categoria: cat, item: item)); + } + } + } + return resultados; + } +} + +class SearchResult { + final RecyclingCategory categoria; + final RecyclingItem item; + + const SearchResult({required this.categoria, required this.item}); +} diff --git a/lib/features/recycling_guide/domain/entities/recycling_category.dart b/lib/features/recycling_guide/domain/entities/recycling_category.dart new file mode 100644 index 0000000..7f70fb9 --- /dev/null +++ b/lib/features/recycling_guide/domain/entities/recycling_category.dart @@ -0,0 +1,42 @@ +// lib/features/recycling_guide/domain/entities/recycling_category.dart +// Capa de dominio — cero dependencias de Flutter o paquetes externos. + +class RecyclingItem { + final String nombre; + final String ejemplos; + final bool acepta; // true = sí va aquí, false = NO va aquí + + const RecyclingItem({ + required this.nombre, + required this.ejemplos, + required this.acepta, + }); +} + +class RecyclingCategory { + final String id; + final String nombre; + final String descripcion; + final String colorHex; + final String icono; + final String consejo; + final List items; + + const RecyclingCategory({ + required this.id, + required this.nombre, + required this.descripcion, + required this.colorHex, + required this.icono, + required this.consejo, + required this.items, + }); + + /// Items que SÍ van en esta categoría + List get itemsAceptados => + items.where((i) => i.acepta).toList(); + + /// Items que NO van en esta categoría + List get itemsRechazados => + items.where((i) => !i.acepta).toList(); +} diff --git a/lib/features/recycling_guide/presentation/providers/recycling_provider.dart b/lib/features/recycling_guide/presentation/providers/recycling_provider.dart new file mode 100644 index 0000000..4fb2d54 --- /dev/null +++ b/lib/features/recycling_guide/presentation/providers/recycling_provider.dart @@ -0,0 +1,64 @@ +// lib/features/recycling_guide/presentation/providers/recycling_provider.dart +// Riverpod — compatible con lo que usa Persona C en el resto de la app. + +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../../data/repositories/recycling_repository.dart'; +import '../../domain/entities/recycling_category.dart'; + +// ── Repositorio singleton ──────────────────────────────────────────── +final recyclingRepositoryProvider = Provider( + (ref) => RecyclingRepository(), +); + +// ── Categorías (carga inicial desde JSON) ──────────────────────────── +final recyclingCategoriesProvider = + FutureProvider>((ref) { + return ref.watch(recyclingRepositoryProvider).obtenerCategorias(); +}); + +// ── Estado del buscador ────────────────────────────────────────────── +class RecyclingSearchNotifier extends StateNotifier { + final RecyclingRepository _repo; + + RecyclingSearchNotifier(this._repo) + : super(const RecyclingSearchState.idle()); + + Future buscar(String query) async { + if (query.trim().isEmpty) { + state = const RecyclingSearchState.idle(); + return; + } + state = const RecyclingSearchState.loading(); + final resultados = await _repo.buscar(query); + state = RecyclingSearchState.done(resultados); + } + + void limpiar() => state = const RecyclingSearchState.idle(); +} + +final recyclingSearchProvider = + StateNotifierProvider((ref) { + return RecyclingSearchNotifier(ref.watch(recyclingRepositoryProvider)); +}); + +// ── Estado sellado del buscador ────────────────────────────────────── +sealed class RecyclingSearchState { + const RecyclingSearchState(); + + const factory RecyclingSearchState.idle() = _Idle; + const factory RecyclingSearchState.loading() = _Loading; + const factory RecyclingSearchState.done(List results) = _Done; +} + +class _Idle extends RecyclingSearchState { + const _Idle(); +} + +class _Loading extends RecyclingSearchState { + const _Loading(); +} + +class _Done extends RecyclingSearchState { + final List results; + const _Done(this.results); +} diff --git a/lib/features/recycling_guide/presentation/screens/category_detail_screen.dart b/lib/features/recycling_guide/presentation/screens/category_detail_screen.dart new file mode 100644 index 0000000..7ceb596 --- /dev/null +++ b/lib/features/recycling_guide/presentation/screens/category_detail_screen.dart @@ -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]), + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/features/recycling_guide/presentation/screens/recycling_guide_screen.dart b/lib/features/recycling_guide/presentation/screens/recycling_guide_screen.dart new file mode 100644 index 0000000..10948d9 --- /dev/null +++ b/lib/features/recycling_guide/presentation/screens/recycling_guide_screen.dart @@ -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 createState() => + _RecyclingGuideScreenState(); +} + +class _RecyclingGuideScreenState extends ConsumerState { + 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(), + }; + } +} diff --git a/lib/features/recycling_guide/presentation/widgets/category_card.dart b/lib/features/recycling_guide/presentation/widgets/category_card.dart new file mode 100644 index 0000000..a664d1d --- /dev/null +++ b/lib/features/recycling_guide/presentation/widgets/category_card.dart @@ -0,0 +1,137 @@ +// lib/features/recycling_guide/presentation/widgets/category_card.dart + +import 'package:flutter/material.dart'; +import '../../domain/entities/recycling_category.dart'; + +class CategoryCard extends StatelessWidget { + final RecyclingCategory categoria; + final VoidCallback onTap; + + const CategoryCard({ + super.key, + required this.categoria, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + final color = _parseColor(categoria.colorHex); + + return Card( + clipBehavior: Clip.antiAlias, + child: InkWell( + onTap: onTap, + child: Padding( + padding: const EdgeInsets.all(16), + child: Row( + children: [ + // Ícono con fondo coloreado + Container( + width: 56, + height: 56, + decoration: BoxDecoration( + color: color.withOpacity(0.15), + borderRadius: BorderRadius.circular(12), + ), + child: Icon( + _iconoDesdeNombre(categoria.icono), + color: color, + size: 28, + ), + ), + const SizedBox(width: 16), + // Texto + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + categoria.nombre, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: color, + ), + ), + const SizedBox(height: 4), + Text( + categoria.descripcion, + style: TextStyle( + fontSize: 13, + color: Colors.grey[600], + ), + ), + const SizedBox(height: 8), + // Chip contador de items + Row( + children: [ + _CountChip( + count: categoria.itemsAceptados.length, + label: 'van aquí', + color: Colors.green, + ), + const SizedBox(width: 8), + _CountChip( + count: categoria.itemsRechazados.length, + label: 'no van', + color: Colors.red, + ), + ], + ), + ], + ), + ), + Icon(Icons.chevron_right, color: Colors.grey[400]), + ], + ), + ), + ), + ); + } + + 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 _CountChip extends StatelessWidget { + final int count; + final String label; + final Color color; + + const _CountChip({ + required this.count, + required this.label, + required this.color, + }); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), + decoration: BoxDecoration( + color: color.withOpacity(0.1), + borderRadius: BorderRadius.circular(20), + ), + child: Text( + '$count $label', + style: TextStyle( + fontSize: 11, + color: color, + fontWeight: FontWeight.w500, + ), + ), + ); + } +} diff --git a/lib/features/recycling_guide/presentation/widgets/search_result_tile.dart b/lib/features/recycling_guide/presentation/widgets/search_result_tile.dart new file mode 100644 index 0000000..9f56da8 --- /dev/null +++ b/lib/features/recycling_guide/presentation/widgets/search_result_tile.dart @@ -0,0 +1,60 @@ +// lib/features/recycling_guide/presentation/widgets/search_result_tile.dart + +import 'package:flutter/material.dart'; +import '../../data/repositories/recycling_repository.dart'; + +class SearchResultTile extends StatelessWidget { + final SearchResult resultado; + + const SearchResultTile({super.key, required this.resultado}); + + @override + Widget build(BuildContext context) { + final color = _parseColor(resultado.categoria.colorHex); + final acepta = resultado.item.acepta; + + return ListTile( + leading: Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: color.withOpacity(0.15), + borderRadius: BorderRadius.circular(8), + ), + child: Icon( + acepta ? Icons.check_circle : Icons.cancel, + color: acepta ? Colors.green : Colors.red, + size: 22, + ), + ), + title: Text( + resultado.item.nombre, + style: const TextStyle(fontWeight: FontWeight.w500, fontSize: 14), + ), + subtitle: Text( + resultado.item.ejemplos, + style: TextStyle(fontSize: 12, color: Colors.grey[600]), + ), + trailing: Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: color.withOpacity(0.1), + borderRadius: BorderRadius.circular(12), + ), + child: Text( + resultado.categoria.nombre, + style: TextStyle( + fontSize: 11, + color: color, + fontWeight: FontWeight.w600, + ), + ), + ), + ); + } + + Color _parseColor(String hex) { + final h = hex.replaceFirst('#', ''); + return Color(int.parse('FF$h', radix: 16)); + } +} diff --git a/lib/main.dart b/lib/main.dart index b20b63e..47c67cb 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; -import 'src/views/rutas.dart'; +import 'features/recycling_guide/presentation/screens/recycling_guide_screen.dart'; +import 'theme/app_theme.dart'; void main() { runApp(const MyApp()); @@ -12,98 +13,9 @@ class MyApp extends StatelessWidget { Widget build(BuildContext context) { return MaterialApp( debugShowCheckedModeBanner: false, - title: 'Hackaton App', - home: const RegistroView(), - ); - } -} - -class RegistroView extends StatelessWidget { - const RegistroView({super.key}); - - final Color colorAzul = const Color(0xFF0F0D38); - final Color colorVerde = const Color(0xFF2E4D31); - - @override - Widget build(BuildContext context) { - return Scaffold( - backgroundColor: Colors.white, - appBar: AppBar( - backgroundColor: colorAzul, - title: const Text('Registro', - style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 28)), - centerTitle: true, - ), - body: SingleChildScrollView( - padding: const EdgeInsets.all(30.0), - child: Column( - children: [ - const SizedBox(height: 20), - _buildInput(Icons.person_outline, 'Nombre'), - _buildInput(Icons.person_outline, 'Apellidos'), - _buildInput(Icons.email_outlined, 'Correo'), - _buildInput(Icons.lock_outline, 'Contraseña'), - const SizedBox(height: 50), - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: colorAzul, - padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 15), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)) - ), - onPressed: () { - Navigator.push(context, MaterialPageRoute(builder: (context) => const HorariosView())); - }, - child: const Text('Registrar', style: TextStyle(color: Colors.white, fontSize: 18)), - ), - ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: colorAzul, - padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 15), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)) - ), - onPressed: () {}, - child: const Text('Iniciar Sesion', style: TextStyle(color: Colors.white, fontSize: 18)), - ), - ], - ) - ], - ), - ), - ); - } - - Widget _buildInput(IconData icon, String hint) { - return Padding( - padding: const EdgeInsets.symmetric(vertical: 15), - child: Row( - children: [ - Container( - padding: const EdgeInsets.all(10), - decoration: BoxDecoration( - shape: BoxShape.circle, - border: Border.all(color: Colors.black, width: 2), - ), - child: Icon(icon, size: 40, color: Colors.black), - ), - const SizedBox(width: 15), - Expanded( - child: TextField( - decoration: InputDecoration( - hintText: hint, - hintStyle: TextStyle(color: colorVerde.withOpacity(0.5), fontWeight: FontWeight.bold, fontSize: 22), - contentPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 15), - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(30), - borderSide: BorderSide(color: colorVerde, width: 4), - ), - ), - ), - ), - ], - ), + title: 'Basura App', + theme: AppTheme.lightTheme, + home: const RecyclingGuideScreen(), ); } } \ No newline at end of file diff --git a/lib/src/views/configuracion.dart b/lib/src/views/configuracion.dart deleted file mode 100644 index 4241d15..0000000 --- a/lib/src/views/configuracion.dart +++ /dev/null @@ -1,71 +0,0 @@ -import 'package:flutter/material.dart'; -import 'rutas.dart'; - -class ConfiguracionView extends StatelessWidget { - const ConfiguracionView({super.key}); - - @override - Widget build(BuildContext context) { - return Scaffold( - backgroundColor: Colors.white, - appBar: AppBar( - backgroundColor: colorAzul, - title: const Text('Configuracion', style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold)), - centerTitle: true, - ), - body: Padding( - padding: const EdgeInsets.all(20.0), - child: Column( - children: [ - Container( - padding: const EdgeInsets.all(15), - decoration: BoxDecoration( - border: Border.all(color: Colors.black, width: 4), - borderRadius: BorderRadius.circular(25), - ), - child: const Row( - children: [ - Icon(Icons.notifications_active_outlined, size: 60), - SizedBox(width: 10), - Text('{Rango dias}', style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold)), - Spacer(), - Icon(Icons.keyboard_arrow_down, size: 50) - ], - ), - ), - ], - ), - ), - bottomNavigationBar: _customNavBarConfig(context), - ); - } - - Widget _customNavBarConfig(BuildContext context) { - return Container( - height: 90, color: colorAzul, - child: Stack( - alignment: Alignment.center, - clipBehavior: Clip.none, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - IconButton(onPressed: () {}, icon: const Icon(Icons.home_outlined, color: Colors.white, size: 40)), - IconButton(onPressed: () => Navigator.push(context, MaterialPageRoute(builder: (context) => const HorariosView())), - icon: const Icon(Icons.alt_route_rounded, color: Colors.white, size: 40)), - const SizedBox(width: 80), - ], - ), - Positioned( - top: -25, right: 30, // Posición según img 3 - child: Container( - width: 85, height: 85, - decoration: BoxDecoration(color: Colors.white, shape: BoxShape.circle, border: Border.all(color: colorVerde, width: 4)), - child: const Icon(Icons.settings, color: colorVerde, size: 45), - ), - ), - ], - ), - ); - } -} \ No newline at end of file diff --git a/lib/src/views/domicilios.dart b/lib/src/views/domicilios.dart deleted file mode 100644 index 97ba5ce..0000000 --- a/lib/src/views/domicilios.dart +++ /dev/null @@ -1,87 +0,0 @@ -import 'package:flutter/material.dart'; -import 'rutas.dart'; - -class DomiciliosView extends StatelessWidget { - const DomiciliosView({super.key}); - - @override - Widget build(BuildContext context) { - return Scaffold( - backgroundColor: Colors.white, - appBar: AppBar( - backgroundColor: colorAzul, - title: const Text('Domicilios', style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold)), - centerTitle: true, - ), - body: Padding( - padding: const EdgeInsets.all(20.0), - child: Column( - children: [ - Container( - padding: const EdgeInsets.all(15), - decoration: BoxDecoration( - border: Border.all(color: Colors.black, width: 4), - borderRadius: BorderRadius.circular(25), - ), - child: Row( - children: [ - const Icon(Icons.home_outlined, size: 60), - const SizedBox(width: 10), - const Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text('{NAME}', style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold)), - Text('Colonia y Ruta', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), - ], - ), - const Spacer(), - IconButton(onPressed: () {}, icon: const Icon(Icons.more_horiz, size: 40)) - ], - ), - ), - const SizedBox(height: 20), - Container( - width: double.infinity, - height: 100, - decoration: BoxDecoration( - color: colorAzul, - borderRadius: BorderRadius.circular(20), - ), - child: const Icon(Icons.add, color: Colors.black, size: 80), - ), - ], - ), - ), - bottomNavigationBar: _customNavBarDomicilio(context), - ); - } - - Widget _customNavBarDomicilio(BuildContext context) { - return Container( - height: 90, color: colorAzul, - child: Stack( - alignment: Alignment.center, - clipBehavior: Clip.none, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - const SizedBox(width: 80), - IconButton(onPressed: () => Navigator.push(context, MaterialPageRoute(builder: (context) => const HorariosView())), - icon: const Icon(Icons.alt_route_rounded, color: Colors.white, size: 40)), - IconButton(onPressed: () {}, icon: const Icon(Icons.notifications_none, color: Colors.white, size: 40)), - ], - ), - Positioned( - top: -25, left: 30, // Posición según img 2 - child: Container( - width: 85, height: 85, - decoration: BoxDecoration(color: Colors.white, shape: BoxShape.circle, border: Border.all(color: colorVerde, width: 4)), - child: const Icon(Icons.home, color: colorVerde, size: 45), - ), - ), - ], - ), - ); - } -} \ No newline at end of file diff --git a/lib/src/views/horarios.dart b/lib/src/views/horarios.dart deleted file mode 100644 index 85a7f8e..0000000 --- a/lib/src/views/horarios.dart +++ /dev/null @@ -1,148 +0,0 @@ -import 'package:flutter/material.dart'; - -class HorariosView extends StatelessWidget { - const HorariosView({super.key}); - - @override - Widget build(BuildContext context) { - const color = Color(0xFF0F0D38); - - final List> rutas = List.generate( - 6, - (index) => { - 'colonia': 'Colonia', - 'ruta': 'Ruta', - 'horario': '(horario estimado)', - }, - ); - - return Scaffold( - backgroundColor: Colors.white, - - appBar: AppBar( - backgroundColor: color, - title: const Text( - 'Horarios', - style: TextStyle( - color: Colors.white, - fontSize: 32, - fontWeight: FontWeight.bold, - ), - ), - centerTitle: true, - elevation: 0, - ), - - body: ListView.builder( - padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 24), - itemCount: rutas.length, - itemBuilder: (context, index) { - final item = rutas[index]; - return Padding( - padding: const EdgeInsets.symmetric(vertical: 16.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - item['colonia']!, - style: const TextStyle( - fontSize: 22, - fontWeight: FontWeight.bold, - color: Colors.black, - ), - ), - Text( - item['ruta']!, - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - color: Colors.black, - ), - ), - ], - ), - // Bloque Derecho: Horario estimado - Text( - item['horario']!, - style: const TextStyle( - fontSize: 20, - fontWeight: FontWeight.bold, - color: Colors.black, - ), - ), - ], - ), - ); - }, - ), - - // 3. BARRA INFERIOR (Menú de navegación personalizado) - bottomNavigationBar: Container( - height: 90, - color: colorVerde, - child: Stack( - alignment: Alignment.center, - clipBehavior: - Clip.none, // Permite que el botón central sobresalga hacia arriba - children: [ - // Iconos de los lados - Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - // Botón de Inicio (Izquierda) - IconButton( - icon: const Icon( - Icons.home_outlined, - color: Colors.white, - size: 40, - ), - onPressed: () {}, - ), - // Espacio invisible en medio para que no choque con el botón central grande - const SizedBox(width: 80), - // Botón de Notificaciones (Derecha) - IconButton( - icon: const Icon( - Icons.notifications_none, - color: Colors.white, - size: 40, - ), - onPressed: () {}, - ), - ], - ), - - // Botón Central Flotante y personalizado (Mapa/Ruta) - Positioned( - top: -25, // Lo empuja hacia arriba fuera de la barra verde - child: Container( - width: 85, - height: 85, - decoration: BoxDecoration( - color: Colors.white, - shape: BoxShape.circle, - border: Border.all(color: colorVerde, width: 4), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.3), - blurRadius: 8, - offset: const Offset(0, 4), - ), - ], - ), - child: const Icon( - Icons.alt_route_rounded, // Icono similar de caminos/puntos - color: colorVerde, - size: 45, - ), - ), - ), - ], - ), - ), - ); - } -} \ No newline at end of file diff --git a/lib/src/views/rutas.dart b/lib/src/views/rutas.dart deleted file mode 100644 index f297885..0000000 --- a/lib/src/views/rutas.dart +++ /dev/null @@ -1,99 +0,0 @@ -import 'package:flutter/material.dart'; -import 'domicilios.dart'; -import 'configuracion.dart'; - -const Color colorVerde = Color(0xFF2E4D31); -const Color colorAzul = Color(0xFF0F0D38); - -class HorariosView extends StatelessWidget { - const HorariosView({super.key}); - - @override - Widget build(BuildContext context) { - final List> rutas = List.generate( - 6, - (index) => { - 'colonia': 'Colonia', - 'ruta': 'Ruta', - 'horario': '(horario estimado)', - }, - ); - - return Scaffold( - backgroundColor: Colors.white, - appBar: AppBar( - backgroundColor: colorAzul, - title: const Text('Horarios', - style: TextStyle(color: Colors.white, fontSize: 32, fontWeight: FontWeight.bold)), - centerTitle: true, - ), - body: ListView.builder( - padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 24), - itemCount: rutas.length, - itemBuilder: (context, index) { - final item = rutas[index]; - return Padding( - padding: const EdgeInsets.symmetric(vertical: 16.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(item['colonia']!, style: const TextStyle(fontSize: 22, fontWeight: FontWeight.bold)), - Text(item['ruta']!, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold)), - ], - ), - Text(item['horario']!, style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold)), - ], - ), - ); - }, - ), - bottomNavigationBar: _customNavBar(context, 1), - ); - } -} - -// Widget reutilizable para la barra inferior de las 3 vistas -Widget _customNavBar(BuildContext context, int activeIndex) { - return Container( - height: 90, - color: colorAzul, - child: Stack( - alignment: Alignment.center, - clipBehavior: Clip.none, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - IconButton( - icon: Icon(Icons.home_outlined, color: Colors.white, size: activeIndex == 0 ? 0 : 40), - onPressed: () => Navigator.push(context, MaterialPageRoute(builder: (context) => const DomiciliosView())), - ), - const SizedBox(width: 80), - IconButton( - icon: Icon(activeIndex == 2 ? Icons.settings : Icons.notifications_none, color: Colors.white, size: activeIndex == 2 ? 0 : 40), - onPressed: () => Navigator.push(context, MaterialPageRoute(builder: (context) => const ConfiguracionView())), - ), - ], - ), - Positioned( - top: -25, - child: Container( - width: 85, height: 85, - decoration: BoxDecoration( - color: Colors.white, - shape: BoxShape.circle, - border: Border.all(color: colorVerde, width: 4), - ), - child: Icon( - activeIndex == 0 ? Icons.home : (activeIndex == 1 ? Icons.alt_route_rounded : Icons.settings), - color: colorVerde, size: 45, - ), - ), - ), - ], - ), - ); -} \ No newline at end of file diff --git a/lib/theme/app_theme.dart b/lib/theme/app_theme.dart new file mode 100644 index 0000000..508f429 --- /dev/null +++ b/lib/theme/app_theme.dart @@ -0,0 +1,78 @@ +// lib/core/theme/app_theme.dart +// Persona C importa este archivo también. +// Un solo lugar para colores, tipografía y estilos. + +import 'package:flutter/material.dart'; + +class AppTheme { + AppTheme._(); + + // ── Paleta de categorías ───────────────────────────────────────── + static const organicosColor = Color(0xFF4CAF50); + static const reciclabesColor = Color(0xFF2196F3); + static const sanitariosColor = Color(0xFFFF5722); + static const especialesColor = Color(0xFFFF9800); + + // ── Paleta general ─────────────────────────────────────────────── + static const primaryColor = Color(0xFF1B5E20); // verde oscuro + static const secondaryColor = Color(0xFF2E7D32); + static const backgroundColor = Color(0xFFF5F5F5); + static const surfaceColor = Color(0xFFFFFFFF); + static const errorColor = Color(0xFFD32F2F); + + static ThemeData get lightTheme => ThemeData( + useMaterial3: true, + colorScheme: ColorScheme.fromSeed( + seedColor: primaryColor, + surface: surfaceColor, + error: errorColor, + ), + scaffoldBackgroundColor: backgroundColor, + appBarTheme: const AppBarTheme( + backgroundColor: primaryColor, + foregroundColor: Colors.white, + elevation: 0, + centerTitle: true, + titleTextStyle: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, + color: Colors.white, + ), + ), + cardTheme: CardTheme( + color: surfaceColor, + elevation: 2, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + chipTheme: ChipThemeData( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + ), + inputDecorationTheme: InputDecorationTheme( + filled: true, + fillColor: surfaceColor, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide.none, + ), + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 12, + ), + ), + ); + + // ── Colores por id de categoría ────────────────────────────────── + static Color colorDeCategoriaId(String id) { + return switch (id) { + 'organicos' => organicosColor, + 'reciclables' => reciclabesColor, + 'sanitarios' => sanitariosColor, + 'especiales' => especialesColor, + _ => primaryColor, + }; + } +}