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:
@@ -1,288 +1,19 @@
|
||||
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/models/auth_state.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) {
|
||||
// ValueNotifier used as refreshListenable so GoRouter re-evaluates redirect
|
||||
// without recreating the router (which would unmount widgets mid-request).
|
||||
final notifier = ValueNotifier<int>(0);
|
||||
ref.listen<AsyncValue<AuthState>>(authControllerProvider, (prev, next) {
|
||||
notifier.value++;
|
||||
});
|
||||
ref.onDispose(notifier.dispose);
|
||||
|
||||
return GoRouter(
|
||||
initialLocation: '/login',
|
||||
refreshListenable: notifier,
|
||||
redirect: (context, state) {
|
||||
final authSnapshot = ref.read(authControllerProvider);
|
||||
final isAuthenticated =
|
||||
authSnapshot.asData?.value.isAuthenticated ?? false;
|
||||
final location = state.matchedLocation;
|
||||
final isAuthRoute =
|
||||
location == '/login' || location == '/register';
|
||||
|
||||
if (authSnapshot.isLoading) return null;
|
||||
if (!isAuthenticated && !isAuthRoute) return '/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(),
|
||||
),
|
||||
],
|
||||
);
|
||||
});
|
||||
import 'package:recolecta_app/core/router/app_router.dart';
|
||||
import 'package:recolecta_app/core/theme/app_theme.dart';
|
||||
|
||||
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)),
|
||||
],
|
||||
),
|
||||
final router = ref.watch(routerProvider);
|
||||
return MaterialApp.router(
|
||||
title: 'Recolecta App',
|
||||
theme: AppTheme.lightTheme,
|
||||
debugShowCheckedModeBanner: false,
|
||||
routerConfig: router,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user