Files
Eventify-frontend/lib/screens/login_screen.dart
Sicherhaven 9dd78be03e fix: make Continue as Guest button visible, guard wishlist for guests
The guest button was nearly invisible (grey text, fontSize 13 on dark
background). Now uses white70, fontSize 15, TextButton with proper
tap padding. Also guards wishlist toggle on event detail page.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-21 07:29:55 +05:30

679 lines
26 KiB
Dart

// lib/screens/login_screen.dart
import 'dart:ui';
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:flutter/material.dart';
import 'package:video_player/video_player.dart';
import '../features/auth/services/auth_service.dart';
import '../core/auth/auth_guard.dart';
import 'home_screen.dart';
class LoginScreen extends StatefulWidget {
const LoginScreen({Key? key}) : super(key: key);
@override
State<LoginScreen> createState() => _LoginScreenState();
}
class _LoginScreenState extends State<LoginScreen> {
final _formKey = GlobalKey<FormState>();
final TextEditingController _emailCtrl = TextEditingController();
final TextEditingController _passCtrl = TextEditingController();
final FocusNode _emailFocus = FocusNode();
final FocusNode _passFocus = FocusNode();
final AuthService _auth = AuthService();
bool _loading = false;
bool _obscurePassword = true;
bool _rememberMe = false;
late VideoPlayerController _videoController;
bool _videoInitialized = false;
// Glassmorphism color palette
static const _darkBg = Color(0xFF0A0A0A);
static const _glassBg = Color(0x1AFFFFFF); // 10% white
static const _glassBorder = Color(0x33FFFFFF); // 20% white
static const _inputBg = Color(0x14FFFFFF); // 8% white
static const _inputBorder = Color(0x26FFFFFF); // 15% white
static const _textWhite = Colors.white;
static const _textMuted = Color(0xFFAAAAAA);
static const _textHint = Color(0xFF888888);
@override
void initState() {
super.initState();
_initVideo();
}
Future<void> _initVideo() async {
_videoController = VideoPlayerController.asset('assets/login-bg.mp4');
await _videoController.initialize();
_videoController.setLooping(true);
_videoController.setVolume(0);
_videoController.play();
if (mounted) setState(() => _videoInitialized = true);
}
@override
void dispose() {
_videoController.dispose();
_emailCtrl.dispose();
_passCtrl.dispose();
_emailFocus.dispose();
_passFocus.dispose();
super.dispose();
}
String? _emailValidator(String? v) {
if (v == null || v.trim().isEmpty) return 'Enter email';
final email = v.trim();
final emailRegex = RegExp(r"^[^@]+@[^@]+\.[^@]+");
if (!emailRegex.hasMatch(email)) return 'Enter a valid email';
return null;
}
String? _passwordValidator(String? v) {
if (v == null || v.trim().isEmpty) return 'Enter password';
if (v.length < 6) return 'Password must be at least 6 characters';
return null;
}
Future<void> _performLogin() async {
final form = _formKey.currentState;
if (form == null) return;
if (!form.validate()) return;
final email = _emailCtrl.text.trim();
final password = _passCtrl.text;
setState(() => _loading = true);
try {
await _auth.login(email, password);
if (!mounted) return;
await Future.delayed(const Duration(milliseconds: 150));
Navigator.of(context).pushReplacement(PageRouteBuilder(
pageBuilder: (context, a1, a2) => const HomeScreen(),
transitionDuration: const Duration(milliseconds: 650),
reverseTransitionDuration: const Duration(milliseconds: 350),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return FadeTransition(opacity: animation, child: child);
},
));
} catch (e) {
if (!mounted) return;
final message = e.toString().replaceAll('Exception: ', '');
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(message)));
} finally {
if (mounted) setState(() => _loading = false);
}
}
void _openRegister() {
Navigator.of(context).push(MaterialPageRoute(builder: (_) => const RegisterScreen(isDesktop: false)));
}
void _showComingSoon() {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Coming soon'), duration: Duration(seconds: 1)),
);
}
/// Glassmorphism pill-shaped input decoration
InputDecoration _glassInputDecoration({
required String hint,
required IconData prefixIcon,
Widget? suffixIcon,
}) {
const borderRadius = BorderRadius.all(Radius.circular(28));
return InputDecoration(
hintText: hint,
hintStyle: const TextStyle(color: _textHint, fontSize: 14),
prefixIcon: Padding(
padding: const EdgeInsets.only(left: 16, right: 8),
child: Icon(prefixIcon, color: _textMuted, size: 20),
),
prefixIconConstraints: const BoxConstraints(minWidth: 44),
suffixIcon: suffixIcon != null
? Padding(
padding: const EdgeInsets.only(right: 8),
child: suffixIcon,
)
: null,
filled: true,
fillColor: _inputBg,
contentPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
border: OutlineInputBorder(
borderRadius: borderRadius,
borderSide: BorderSide(color: _inputBorder),
),
enabledBorder: OutlineInputBorder(
borderRadius: borderRadius,
borderSide: BorderSide(color: _inputBorder),
),
focusedBorder: OutlineInputBorder(
borderRadius: borderRadius,
borderSide: BorderSide(color: _glassBorder, width: 1.5),
),
errorBorder: OutlineInputBorder(
borderRadius: borderRadius,
borderSide: const BorderSide(color: Colors.redAccent),
),
focusedErrorBorder: OutlineInputBorder(
borderRadius: borderRadius,
borderSide: const BorderSide(color: Colors.redAccent, width: 1.5),
),
errorStyle: const TextStyle(color: Colors.redAccent, fontSize: 11),
);
}
/// Glassmorphism social button
Widget _socialButton({
required String label,
required Widget icon,
required VoidCallback onTap,
}) {
return Expanded(
child: GestureDetector(
onTap: onTap,
child: Container(
padding: const EdgeInsets.symmetric(vertical: 14),
decoration: BoxDecoration(
color: _inputBg,
borderRadius: BorderRadius.circular(28),
border: Border.all(color: _inputBorder),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
icon,
const SizedBox(width: 8),
Text(
label,
style: const TextStyle(
color: _textWhite,
fontSize: 13,
fontWeight: FontWeight.w500,
),
),
],
),
),
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: _darkBg,
body: Stack(
children: [
// Video background
if (_videoInitialized)
Positioned.fill(
child: FittedBox(
fit: BoxFit.cover,
child: SizedBox(
width: _videoController.value.size.width,
height: _videoController.value.size.height,
child: VideoPlayer(_videoController),
),
),
),
// Dark gradient overlay for readability
Positioned.fill(
child: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.black.withOpacity(0.50),
Colors.black.withOpacity(0.65),
Colors.black.withOpacity(0.70),
],
stops: const [0.0, 0.4, 1.0],
),
),
),
),
// Main content
SafeArea(
child: Center(
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 20),
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 400),
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Brand name
Center(
child: Text(
'Eventify',
style: TextStyle(
color: _textWhite.withOpacity(0.7),
fontSize: 16,
fontWeight: FontWeight.w500,
fontStyle: FontStyle.italic,
letterSpacing: 1.5,
),
),
),
const SizedBox(height: 12),
// Heading
const Center(
child: Text(
'Log In, Start Your\nJourney',
textAlign: TextAlign.center,
style: TextStyle(
color: _textWhite,
fontSize: 28,
fontWeight: FontWeight.bold,
fontStyle: FontStyle.italic,
height: 1.2,
),
),
),
const SizedBox(height: 32),
// Email label
const Padding(
padding: EdgeInsets.only(left: 4, bottom: 8),
child: Text(
'Email',
style: TextStyle(color: _textMuted, fontSize: 13),
),
),
// Email input
TextFormField(
controller: _emailCtrl,
focusNode: _emailFocus,
keyboardType: TextInputType.emailAddress,
autofillHints: const [AutofillHints.email],
style: const TextStyle(color: _textWhite, fontSize: 14),
cursorColor: Colors.white54,
decoration: _glassInputDecoration(
hint: 'Enter your email',
prefixIcon: Icons.mail_outline_rounded,
),
validator: _emailValidator,
textInputAction: TextInputAction.next,
onFieldSubmitted: (_) {
FocusScope.of(context).requestFocus(_passFocus);
},
),
const SizedBox(height: 18),
// Password label
const Padding(
padding: EdgeInsets.only(left: 4, bottom: 8),
child: Text(
'Password',
style: TextStyle(color: _textMuted, fontSize: 13),
),
),
// Password input
TextFormField(
controller: _passCtrl,
focusNode: _passFocus,
obscureText: _obscurePassword,
style: const TextStyle(color: _textWhite, fontSize: 14),
cursorColor: Colors.white54,
decoration: _glassInputDecoration(
hint: 'Enter your password',
prefixIcon: Icons.lock_outline_rounded,
suffixIcon: IconButton(
icon: Icon(
_obscurePassword ? Icons.visibility_off_outlined : Icons.visibility_outlined,
color: _textMuted,
size: 20,
),
onPressed: () => setState(() => _obscurePassword = !_obscurePassword),
),
),
validator: _passwordValidator,
textInputAction: TextInputAction.done,
onFieldSubmitted: (_) => _performLogin(),
),
const SizedBox(height: 14),
// Remember me + Forgot Password row
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
// Remember me
GestureDetector(
onTap: () => setState(() => _rememberMe = !_rememberMe),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 18,
height: 18,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(4),
border: Border.all(color: _glassBorder),
color: _rememberMe ? Colors.white24 : Colors.transparent,
),
child: _rememberMe
? const Icon(Icons.check, size: 14, color: _textWhite)
: null,
),
const SizedBox(width: 8),
const Text(
'Remember me',
style: TextStyle(color: _textMuted, fontSize: 12),
),
],
),
),
// Forgot Password
GestureDetector(
onTap: _showComingSoon,
child: const Text(
'Forgot Password?',
style: TextStyle(color: _textMuted, fontSize: 12),
),
),
],
),
const SizedBox(height: 24),
// Login button — dark gradient pill
SizedBox(
width: double.infinity,
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(28),
gradient: const LinearGradient(
colors: [Color(0xFF2A2A2A), Color(0xFF1A1A1A)],
),
border: Border.all(color: const Color(0x33FFFFFF)),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.4),
blurRadius: 16,
offset: const Offset(0, 6),
),
],
),
child: Material(
color: Colors.transparent,
child: InkWell(
borderRadius: BorderRadius.circular(28),
onTap: _loading ? null : _performLogin,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 16),
child: Center(
child: _loading
? const SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
color: _textWhite,
),
)
: const Text(
'Login',
style: TextStyle(
color: _textWhite,
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
),
),
),
),
),
),
const SizedBox(height: 24),
// "Or continue with" divider
Row(
children: [
Expanded(child: Divider(color: Colors.white.withOpacity(0.12))),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 14),
child: Text(
'Or continue with',
style: TextStyle(
color: _textMuted.withOpacity(0.7),
fontSize: 12,
),
),
),
Expanded(child: Divider(color: Colors.white.withOpacity(0.12))),
],
),
const SizedBox(height: 18),
// Social buttons — side by side
Row(
children: [
_socialButton(
label: 'Google',
icon: const Text(
'G',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Color(0xFF4285F4),
),
),
onTap: _showComingSoon,
),
const SizedBox(width: 12),
_socialButton(
label: 'Apple',
icon: const Icon(Icons.apple, color: _textWhite, size: 20),
onTap: _showComingSoon,
),
],
),
const SizedBox(height: 28),
// Don't have an account? Create an account
Center(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Text(
"Don't have an account? ",
style: TextStyle(color: _textMuted, fontSize: 13),
),
GestureDetector(
onTap: _openRegister,
child: const Text(
'Create an account',
style: TextStyle(
color: _textWhite,
fontSize: 13,
fontWeight: FontWeight.w600,
decoration: TextDecoration.underline,
decorationColor: _textWhite,
),
),
),
],
),
),
const SizedBox(height: 16),
// Continue as Guest
Center(
child: TextButton(
onPressed: () {
AuthGuard.setGuest(true);
Navigator.of(context).pushAndRemoveUntil(
MaterialPageRoute(builder: (_) => const HomeScreen()),
(route) => false,
);
},
style: TextButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
),
child: const Text(
'Continue as Guest',
style: TextStyle(
color: Colors.white70,
fontSize: 15,
fontWeight: FontWeight.w500,
decoration: TextDecoration.underline,
decorationColor: Colors.white70,
),
),
),
),
],
),
),
),
),
),
),
],
),
);
}
}
/// Register screen calls backend register endpoint via AuthService.register
class RegisterScreen extends StatefulWidget {
final bool isDesktop;
const RegisterScreen({Key? key, this.isDesktop = false}) : super(key: key);
@override
State<RegisterScreen> createState() => _RegisterScreenState();
}
class _RegisterScreenState extends State<RegisterScreen> {
final _formKey = GlobalKey<FormState>();
final TextEditingController _emailCtrl = TextEditingController();
final TextEditingController _phoneCtrl = TextEditingController();
final TextEditingController _passCtrl = TextEditingController();
final TextEditingController _confirmCtrl = TextEditingController();
final AuthService _auth = AuthService();
bool _loading = false;
@override
void dispose() {
_emailCtrl.dispose();
_phoneCtrl.dispose();
_passCtrl.dispose();
_confirmCtrl.dispose();
super.dispose();
}
Future<void> _performRegister() async {
if (!(_formKey.currentState?.validate() ?? false)) return;
final email = _emailCtrl.text.trim();
final phone = _phoneCtrl.text.trim();
final pass = _passCtrl.text;
final confirm = _confirmCtrl.text;
if (pass != confirm) {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Passwords do not match')));
return;
}
setState(() => _loading = true);
try {
await _auth.register(
email: email,
phoneNumber: phone,
password: pass,
);
if (!mounted) return;
Navigator.of(context).pushReplacement(MaterialPageRoute(builder: (_) => const HomeScreen()));
} catch (e) {
if (!mounted) return;
final message = e.toString().replaceAll('Exception: ', '');
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(message)));
} finally {
if (mounted) setState(() => _loading = false);
}
}
String? _emailValidator(String? v) {
if (v == null || v.trim().isEmpty) return 'Enter email';
final emailRegex = RegExp(r"^[^@]+@[^@]+\.[^@]+");
if (!emailRegex.hasMatch(v.trim())) return 'Enter a valid email';
return null;
}
String? _phoneValidator(String? v) {
if (v == null || v.trim().isEmpty) return 'Enter phone number';
if (v.trim().length < 7) return 'Enter a valid phone number';
return null;
}
String? _passwordValidator(String? v) {
if (v == null || v.isEmpty) return 'Enter password';
if (v.length < 6) return 'Password must be at least 6 characters';
return null;
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Register')),
body: SafeArea(
child: Center(
child: SingleChildScrollView(
padding: const EdgeInsets.all(20),
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 520),
child: Card(
child: Padding(
padding: const EdgeInsets.all(18.0),
child: Form(
key: _formKey,
child: Column(
children: [
TextFormField(controller: _emailCtrl, decoration: const InputDecoration(labelText: 'Email'), validator: _emailValidator, keyboardType: TextInputType.emailAddress),
const SizedBox(height: 8),
TextFormField(controller: _phoneCtrl, decoration: const InputDecoration(labelText: 'Phone'), validator: _phoneValidator, keyboardType: TextInputType.phone),
const SizedBox(height: 8),
TextFormField(controller: _passCtrl, obscureText: true, decoration: const InputDecoration(labelText: 'Password'), validator: _passwordValidator),
const SizedBox(height: 8),
TextFormField(controller: _confirmCtrl, obscureText: true, decoration: const InputDecoration(labelText: 'Confirm password'), validator: _passwordValidator),
const SizedBox(height: 16),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: _loading ? null : _performRegister,
child: _loading ? const SizedBox(width: 20, height: 20, child: CircularProgressIndicator(color: Colors.white, strokeWidth: 2)) : const Text('Register'),
),
),
],
),
),
),
),
),
),
),
),
);
}
}