Files
hackathon-innovaflow5.0-cdf…/recolecta_app/lib/app/app.dart

287 lines
8.0 KiB
Dart

import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../core/network/api_client.dart';
import '../core/services/auth_controller.dart';
import '../core/storage/secure_storage.dart';
import 'bootstrap.dart' as bootstrap;
import '../features/auth/login_page.dart';
import '../features/auth/register_page.dart';
import '../features/addresses/new_address_page.dart';
final routerProvider = Provider<GoRouter>((ref) {
final authSnapshot = ref.watch(authControllerProvider);
final isAuthenticated = authSnapshot.asData?.value.isAuthenticated ?? false;
return GoRouter(
initialLocation: '/home',
redirect: (context, state) {
final location = state.matchedLocation;
final isAuthRoute = location == '/login' || location == '/register';
if (authSnapshot.isLoading) {
return location == '/login' ? null : '/login';
}
if (!isAuthenticated) {
return isAuthRoute ? null : '/login';
}
if (isAuthenticated && isAuthRoute) {
return '/home';
}
return null;
},
routes: <RouteBase>[
GoRoute(
path: '/login',
name: 'login',
builder: (context, state) => const LoginPage(),
),
GoRoute(
path: '/register',
name: 'register',
builder: (context, state) => const RegisterPage(),
),
GoRoute(
path: '/home',
name: 'home',
builder: (context, state) => const HomePage(),
),
GoRoute(
path: '/addresses/new',
name: 'addresses-new',
builder: (context, state) => const NewAddressPage(),
),
GoRoute(
path: '/status',
name: 'status',
builder: (context, state) => const StatusPage(),
),
],
);
});
class RecolectaApp extends ConsumerWidget {
const RecolectaApp({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final bootstrapState = ref.watch(bootstrap.bootstrapProvider);
return bootstrapState.when(
loading: () => const MaterialApp(
debugShowCheckedModeBanner: false,
home: BootstrapLoadingPage(),
),
error: (error, stackTrace) => MaterialApp(
debugShowCheckedModeBanner: false,
home: BootstrapErrorPage(error: error),
),
data: (_) => MaterialApp.router(
debugShowCheckedModeBanner: false,
title: 'Recolecta',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF1F6F78)),
scaffoldBackgroundColor: const Color(0xFFF4F7F6),
useMaterial3: true,
),
routerConfig: ref.watch(routerProvider),
),
);
}
}
class BootstrapLoadingPage extends StatelessWidget {
const BootstrapLoadingPage({super.key});
@override
Widget build(BuildContext context) {
return const Scaffold(body: Center(child: CircularProgressIndicator()));
}
}
class BootstrapErrorPage extends StatelessWidget {
const BootstrapErrorPage({super.key, required this.error});
final Object error;
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.error_outline, size: 48),
const SizedBox(height: 16),
const Text(
'No se pudo cargar la configuración inicial.',
textAlign: TextAlign.center,
),
const SizedBox(height: 12),
Text(
error.toString(),
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.bodySmall,
),
],
),
),
),
);
}
}
class HomePage extends ConsumerWidget {
const HomePage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final dio = ref.read(apiClientProvider);
final storage = ref.read(secureStorageProvider);
final baseUrl = dio.options.baseUrl;
final authState = ref.watch(authControllerProvider);
return Scaffold(
appBar: AppBar(
title: const Text('Recolecta'),
actions: [
IconButton(
onPressed: authState.isLoading
? null
: () async {
await ref.read(authControllerProvider.notifier).logout();
if (context.mounted) {
context.go('/login');
}
},
icon: const Icon(Icons.logout),
tooltip: 'Salir',
),
IconButton(
onPressed: () => context.goNamed('status'),
icon: const Icon(Icons.route),
tooltip: 'Estado',
),
],
),
body: Padding(
padding: const EdgeInsets.all(24),
child: ListView(
children: [
const Text(
'Bootstrap listo',
style: TextStyle(fontSize: 28, fontWeight: FontWeight.w700),
),
const SizedBox(height: 8),
Text(
'La app ya carga .env, Riverpod y GoRouter para la base del MVP.',
style: Theme.of(context).textTheme.bodyLarge,
),
const SizedBox(height: 16),
TextButton(
onPressed: () => context.go('/addresses/new'),
child: const Text('Agregar domicilio'),
),
const SizedBox(height: 24),
_InfoCard(title: 'API base URL', value: baseUrl, icon: Icons.cloud),
const SizedBox(height: 16),
_InfoCard(
title: 'Secure Storage',
value: storage.runtimeType.toString(),
icon: Icons.lock,
),
const SizedBox(height: 16),
_ImageCard(
title: 'Widget listo para caché de imágenes',
imageUrl:
'https://images.unsplash.com/photo-1542838132-92c53300491e?auto=format&fit=crop&w=800&q=80',
),
],
),
),
);
}
}
class StatusPage extends StatelessWidget {
const StatusPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Estado')),
body: const Padding(
padding: EdgeInsets.all(24),
child: Text(
'Aquí después se conectarán ETA, notificaciones, ruta asignada y métricas de privacidad.',
),
),
);
}
}
class _InfoCard extends StatelessWidget {
const _InfoCard({
required this.title,
required this.value,
required this.icon,
});
final String title;
final String value;
final IconData icon;
@override
Widget build(BuildContext context) {
return Card(
elevation: 0,
child: ListTile(
leading: Icon(icon),
title: Text(title),
subtitle: Text(value),
),
);
}
}
class _ImageCard extends StatelessWidget {
const _ImageCard({required this.title, required this.imageUrl});
final String title;
final String imageUrl;
@override
Widget build(BuildContext context) {
return Card(
clipBehavior: Clip.antiAlias,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
CachedNetworkImage(
imageUrl: imageUrl,
height: 180,
width: double.infinity,
fit: BoxFit.cover,
placeholder: (context, url) => const SizedBox(
height: 180,
child: Center(child: CircularProgressIndicator()),
),
errorWidget: (context, url, error) => const SizedBox(
height: 180,
child: Center(child: Icon(Icons.image_not_supported)),
),
),
Padding(padding: const EdgeInsets.all(16), child: Text(title)),
],
),
);
}
}