Co-authored-by: MENDOZA BALLARDO GAEL RICARDO <gael-meb123@users.noreply.github.com>
Co-authored-by: Azareth-Tr <Azareth-Tr@users.noreply.github.com> Co-authored-by: eddgranados12 <eddgranados12@users.noreply.github.com> implementacion de login, vistas, correcion de errores en vista registro, domicilios
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
// Esta guía funciona offline. El chat con IA (mascota) es la capa extra cuando hay conexión — ver features/mascota/
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class SeparationGuide {
|
||||
final String version;
|
||||
final List<Category> categorias;
|
||||
|
||||
SeparationGuide({required this.version, required this.categorias});
|
||||
|
||||
factory SeparationGuide.fromJson(Map<String, dynamic> json) {
|
||||
return SeparationGuide(
|
||||
version: json['version'],
|
||||
categorias: (json['categorias'] as List)
|
||||
.map((category) => Category.fromJson(category))
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class Category {
|
||||
final String id;
|
||||
final String nombre;
|
||||
final String color;
|
||||
final String icono;
|
||||
final String descripcion;
|
||||
final List<Example> ejemplos;
|
||||
final String consejo;
|
||||
|
||||
Category({
|
||||
required this.id,
|
||||
required this.nombre,
|
||||
required this.color,
|
||||
required this.icono,
|
||||
required this.descripcion,
|
||||
required this.ejemplos,
|
||||
required this.consejo,
|
||||
});
|
||||
|
||||
factory Category.fromJson(Map<String, dynamic> json) {
|
||||
return Category(
|
||||
id: json['id'],
|
||||
nombre: json['nombre'],
|
||||
color: json['color'],
|
||||
icono: json['icono'],
|
||||
descripcion: json['descripcion'],
|
||||
ejemplos: (json['ejemplos'] as List)
|
||||
.map((example) => Example.fromJson(example))
|
||||
.toList(),
|
||||
consejo: json['consejo'],
|
||||
);
|
||||
}
|
||||
|
||||
IconData get iconData {
|
||||
switch (icono) {
|
||||
case 'eco':
|
||||
return Icons.eco;
|
||||
case 'recycling':
|
||||
return Icons.recycling;
|
||||
case 'delete':
|
||||
return Icons.delete;
|
||||
case 'warning':
|
||||
return Icons.warning;
|
||||
default:
|
||||
return Icons.help;
|
||||
}
|
||||
}
|
||||
|
||||
Color get colorValue {
|
||||
return Color(int.parse(color.substring(1, 7), radix: 16) + 0xFF000000);
|
||||
}
|
||||
}
|
||||
|
||||
class Example {
|
||||
final String nombre;
|
||||
final bool acepta;
|
||||
final String? razon;
|
||||
|
||||
Example({required this.nombre, required this.acepta, this.razon});
|
||||
|
||||
factory Example.fromJson(Map<String, dynamic> json) {
|
||||
return Example(
|
||||
nombre: json['nombre'],
|
||||
acepta: json['acepta'],
|
||||
razon: json['razon'],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:recolecta_app/features/separation_guide/models/separation_guide_model.dart';
|
||||
|
||||
// Esta guía funciona offline. El chat con IA (mascota) es la capa extra cuando hay conexión — ver features/mascota/
|
||||
final separationGuideProvider = FutureProvider<SeparationGuide>((ref) async {
|
||||
// keepAlive: provider is non-autoDispose to avoid reloading the JSON on
|
||||
// each navigation. This makes the guide work offline without repeated IO.
|
||||
final jsonString = await rootBundle.loadString(
|
||||
'assets/data/separation_guide.json',
|
||||
);
|
||||
final jsonResponse = json.decode(jsonString) as Map<String, dynamic>;
|
||||
return SeparationGuide.fromJson(jsonResponse);
|
||||
});
|
||||
@@ -0,0 +1,91 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:recolecta_app/features/separation_guide/models/separation_guide_model.dart'
|
||||
as guide;
|
||||
import 'package:recolecta_app/features/separation_guide/providers/separation_guide_provider.dart';
|
||||
|
||||
class CategoryDetailScreen extends ConsumerWidget {
|
||||
final String categoryId;
|
||||
|
||||
const CategoryDetailScreen({super.key, required this.categoryId});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final guideData = ref.watch(separationGuideProvider);
|
||||
|
||||
return Scaffold(
|
||||
body: guideData.when(
|
||||
data: (data) {
|
||||
final category = data.categorias.firstWhere(
|
||||
(c) => c.id == categoryId,
|
||||
);
|
||||
return CustomScrollView(
|
||||
slivers: [
|
||||
SliverAppBar(
|
||||
title: Text(category.nombre),
|
||||
backgroundColor: category.colorValue,
|
||||
expandedHeight: 120,
|
||||
pinned: true,
|
||||
flexibleSpace: FlexibleSpaceBar(
|
||||
background: Container(
|
||||
color: category.colorValue,
|
||||
alignment: Alignment.bottomLeft,
|
||||
padding: const EdgeInsets.only(left: 16, bottom: 24),
|
||||
child: Text(
|
||||
category.descripcion,
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.titleMedium?.copyWith(color: Colors.white),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
SliverList(
|
||||
delegate: SliverChildBuilderDelegate((context, index) {
|
||||
final example = category.ejemplos[index];
|
||||
return ListTile(
|
||||
leading: Icon(
|
||||
example.acepta ? Icons.check_circle : Icons.cancel,
|
||||
color: example.acepta ? Colors.green : Colors.red,
|
||||
),
|
||||
title: Text(example.nombre),
|
||||
subtitle: example.razon != null
|
||||
? Chip(
|
||||
label: Text(example.razon!),
|
||||
backgroundColor: Colors.red.withOpacity(0.1),
|
||||
)
|
||||
: null,
|
||||
);
|
||||
}, childCount: category.ejemplos.length),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error: (err, stack) => Center(child: Text('Error: $err')),
|
||||
),
|
||||
bottomNavigationBar: guideData.maybeWhen(
|
||||
data: (data) {
|
||||
final category = data.categorias.firstWhere(
|
||||
(c) => c.id == categoryId,
|
||||
);
|
||||
return BottomAppBar(
|
||||
color: category.colorValue,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Text(
|
||||
category.consejo,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
orElse: () => null,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:recolecta_app/features/separation_guide/providers/separation_guide_provider.dart';
|
||||
import 'package:recolecta_app/features/separation_guide/models/separation_guide_model.dart'
|
||||
as guide;
|
||||
|
||||
class SeparationGuideScreen extends ConsumerWidget {
|
||||
const SeparationGuideScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final guideData = ref.watch(separationGuideProvider);
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Guía de Separación'),
|
||||
bottom: PreferredSize(
|
||||
preferredSize: const Size.fromHeight(20.0),
|
||||
child: Text(
|
||||
'Funciona sin internet',
|
||||
style: Theme.of(context).textTheme.titleSmall,
|
||||
),
|
||||
),
|
||||
),
|
||||
body: guideData.when(
|
||||
data: (data) => GridView.builder(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 2,
|
||||
crossAxisSpacing: 16.0,
|
||||
mainAxisSpacing: 16.0,
|
||||
childAspectRatio: 1.2,
|
||||
),
|
||||
itemCount: data.categorias.length,
|
||||
itemBuilder: (context, index) {
|
||||
final category = data.categorias[index];
|
||||
return CategoryCard(category: category);
|
||||
},
|
||||
),
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error: (err, stack) => Center(child: Text('Error: $err')),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class CategoryCard extends StatelessWidget {
|
||||
const CategoryCard({super.key, required this.category});
|
||||
|
||||
final guide.Category category;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: () => context.go('/guide/${category.id}'),
|
||||
child: Card(
|
||||
color: category.colorValue.withOpacity(0.15),
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
side: BorderSide(color: category.colorValue, width: 1),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(category.iconData, size: 40, color: category.colorValue),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
category.nombre,
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: category.colorValue,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user