feat:implementacion grafica 2.0

This commit is contained in:
25030248hasel
2026-05-22 18:43:29 -06:00
parent a0f2ce40b1
commit cb005d33f6
22 changed files with 2047 additions and 108 deletions

View File

@@ -0,0 +1,411 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../../core/theme/app_theme.dart';
import '../bloc/auth_bloc.dart';
import '../bloc/auth_event.dart';
import '../bloc/auth_state.dart';
import '../widgets/privacy_notice_card.dart';
/// Pantalla de inicio de sesión — MVP WasteNotify.
///
/// Diseño: Limpio, institucional, con paleta verde-tierra.
/// Incluye mensajería preventiva integrada de forma no intrusiva.
///
/// Credenciales de demo:
/// Ciudadano: ciudadano@ejemplo.com / password123
/// Operador: operador@ejemplo.com / operador456
/// Teléfono: 5551234567 / pass1234
class LoginScreen extends StatefulWidget {
const LoginScreen({super.key});
@override
State<LoginScreen> createState() => _LoginScreenState();
}
class _LoginScreenState extends State<LoginScreen>
with SingleTickerProviderStateMixin {
final _formKey = GlobalKey<FormState>();
final _identifierController = TextEditingController();
final _passwordController = TextEditingController();
bool _obscurePassword = true;
late final AnimationController _animController;
late final Animation<double> _fadeAnim;
late final Animation<Offset> _slideAnim;
@override
void initState() {
super.initState();
_animController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 700),
);
_fadeAnim = CurvedAnimation(
parent: _animController,
curve: Curves.easeOut,
);
_slideAnim = Tween<Offset>(
begin: const Offset(0, 0.06),
end: Offset.zero,
).animate(CurvedAnimation(
parent: _animController,
curve: Curves.easeOutCubic,
));
_animController.forward();
}
@override
void dispose() {
_animController.dispose();
_identifierController.dispose();
_passwordController.dispose();
super.dispose();
}
void _submit(BuildContext context) {
if (_formKey.currentState?.validate() ?? false) {
context.read<AuthBloc>().add(
AuthLoginRequested(
identifier: _identifierController.text,
password: _passwordController.text,
),
);
}
}
@override
Widget build(BuildContext context) {
return BlocListener<AuthBloc, AuthState>(
listener: (context, state) {
if (state is AuthFailure) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Row(
children: [
const Icon(Icons.error_outline, color: Colors.white, size: 18),
const SizedBox(width: 10),
Expanded(child: Text(state.message)),
],
),
backgroundColor: AppTheme.errorRed,
),
);
}
},
child: Scaffold(
backgroundColor: AppTheme.warmWhite,
body: SafeArea(
child: CustomScrollView(
slivers: [
SliverFillRemaining(
hasScrollBody: false,
child: FadeTransition(
opacity: _fadeAnim,
child: SlideTransition(
position: _slideAnim,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 28),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 48),
_buildHeader(),
const SizedBox(height: 32),
const ScheduleWarningBanner(),
const SizedBox(height: 32),
_buildForm(context),
const SizedBox(height: 24),
const PrivacyNoticeCard(),
const SizedBox(height: 32),
_buildDemoHint(),
const SizedBox(height: 24),
],
),
),
),
),
),
],
),
),
),
);
}
Widget _buildHeader() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Logo / ícono principal
Container(
width: 60,
height: 60,
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [AppTheme.leafGreen, AppTheme.forestGreen],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: AppTheme.leafGreen.withOpacity(0.35),
blurRadius: 12,
offset: const Offset(0, 4),
),
],
),
child: const Icon(
Icons.recycling_rounded,
color: Colors.white,
size: 34,
),
),
const SizedBox(height: 20),
RichText(
text: const TextSpan(
style: TextStyle(
fontSize: 28,
fontWeight: FontWeight.w800,
color: AppTheme.charcoal,
height: 1.15,
),
children: [
TextSpan(text: 'Waste'),
TextSpan(
text: 'Notify',
style: TextStyle(color: AppTheme.leafGreen),
),
],
),
),
const SizedBox(height: 8),
Text(
'Notificaciones de recolección\nsin rastreo, sin riesgos.',
style: TextStyle(
fontSize: 14.5,
color: AppTheme.midGray,
height: 1.5,
),
),
],
);
}
Widget _buildForm(BuildContext context) {
return Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// --- Campo: Email o Teléfono ---
TextFormField(
controller: _identifierController,
keyboardType: TextInputType.emailAddress,
textInputAction: TextInputAction.next,
autocorrect: false,
decoration: const InputDecoration(
labelText: 'Correo electrónico o teléfono',
hintText: 'ej. juan@correo.com o 5551234567',
prefixIcon: Icon(Icons.person_outline_rounded),
),
validator: (value) {
if (value == null || value.trim().isEmpty) {
return 'Ingresa tu correo o número de teléfono';
}
final trimmed = value.trim();
final isEmail = RegExp(
r'^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$',
).hasMatch(trimmed);
final isPhone = RegExp(r'^\d{7,15}$').hasMatch(trimmed);
if (!isEmail && !isPhone) {
return 'Ingresa un correo válido o un número de 7-15 dígitos';
}
return null;
},
),
const SizedBox(height: 16),
// --- Campo: Contraseña ---
TextFormField(
controller: _passwordController,
obscureText: _obscurePassword,
textInputAction: TextInputAction.done,
onFieldSubmitted: (_) => _submit(context),
decoration: InputDecoration(
labelText: 'Contraseña',
hintText: 'Mínimo 6 caracteres',
prefixIcon: const Icon(Icons.lock_outline_rounded),
suffixIcon: IconButton(
icon: Icon(
_obscurePassword
? Icons.visibility_off_outlined
: Icons.visibility_outlined,
color: AppTheme.midGray,
),
onPressed: () =>
setState(() => _obscurePassword = !_obscurePassword),
tooltip: _obscurePassword
? 'Mostrar contraseña'
: 'Ocultar contraseña',
),
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Ingresa tu contraseña';
}
if (value.length < 6) {
return 'La contraseña debe tener al menos 6 caracteres';
}
return null;
},
),
const SizedBox(height: 8),
// --- Olvidé mi contraseña ---
Align(
alignment: Alignment.centerRight,
child: TextButton(
onPressed: () {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Recuperación de contraseña — Próximamente'),
),
);
},
child: const Text(
'¿Olvidaste tu contraseña?',
style: TextStyle(fontSize: 13),
),
),
),
const SizedBox(height: 16),
// --- Botón Principal ---
BlocBuilder<AuthBloc, AuthState>(
builder: (context, state) {
final isLoading = state is AuthLoading;
return AnimatedContainer(
duration: const Duration(milliseconds: 200),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
boxShadow: isLoading
? []
: [
BoxShadow(
color: AppTheme.leafGreen.withOpacity(0.4),
blurRadius: 16,
offset: const Offset(0, 6),
),
],
),
child: ElevatedButton(
onPressed: isLoading ? null : () => _submit(context),
style: ElevatedButton.styleFrom(
backgroundColor: isLoading
? AppTheme.mintGreen.withOpacity(0.7)
: AppTheme.leafGreen,
disabledBackgroundColor:
AppTheme.mintGreen.withOpacity(0.6),
),
child: isLoading
? const SizedBox(
height: 22,
width: 22,
child: CircularProgressIndicator(
strokeWidth: 2.5,
valueColor: AlwaysStoppedAnimation(Colors.white),
),
)
: const Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.login_rounded, size: 20),
SizedBox(width: 10),
Text('Ingresar de forma segura'),
],
),
),
);
},
),
],
),
);
}
Widget _buildDemoHint() {
return Container(
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: AppTheme.sandBeige,
borderRadius: BorderRadius.circular(10),
border: Border.all(color: const Color(0xFFFFE082), width: 1),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const Icon(Icons.science_outlined,
size: 15, color: AppTheme.earthBrown),
const SizedBox(width: 6),
Text(
'Demo MVP — Credenciales de prueba',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w700,
color: AppTheme.earthBrown,
),
),
],
),
const SizedBox(height: 6),
_DemoCredential(
label: 'Ciudadano',
user: 'ciudadano@ejemplo.com',
pass: 'password123',
),
_DemoCredential(
label: 'Operador',
user: 'operador@ejemplo.com',
pass: 'operador456',
),
_DemoCredential(
label: 'Teléfono',
user: '5551234567',
pass: 'pass1234',
),
],
),
);
}
}
class _DemoCredential extends StatelessWidget {
final String label;
final String user;
final String pass;
const _DemoCredential({
required this.label,
required this.user,
required this.pass,
});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(top: 3),
child: Text(
'$label: $user / $pass',
style: const TextStyle(
fontSize: 11.5,
color: AppTheme.earthBrown,
fontFamily: 'monospace',
),
),
);
}
}