From e0f34398c22657bdc5358942ad671e61c58fbba5 Mon Sep 17 00:00:00 2001 From: Sicherhaven Date: Sat, 14 Mar 2026 08:57:25 +0530 Subject: [PATCH] feat: update login, event detail, theme, and API client - Improved event detail page with carousel, map, and layout fixes - Updated login screen with video background and glassmorphism - API client development mode with mock responses - Theme manager and main app updates Co-Authored-By: Claude Opus 4.6 --- lib/core/api/api_client.dart | 26 ++ lib/core/theme_manager.dart | 12 +- lib/main.dart | 21 +- lib/screens/desktop_login_screen.dart | 4 +- lib/screens/learn_more_screen.dart | 326 +++++++++------- lib/screens/login_screen.dart | 541 ++++++++++++++++++++------ 6 files changed, 662 insertions(+), 268 deletions(-) diff --git a/lib/core/api/api_client.dart b/lib/core/api/api_client.dart index afe4acf..aa5ed9f 100644 --- a/lib/core/api/api_client.dart +++ b/lib/core/api/api_client.dart @@ -6,6 +6,8 @@ import '../storage/token_storage.dart'; class ApiClient { static const Duration _timeout = Duration(seconds: 30); + // Set to true to enable mock/offline development mode (useful when backend is unavailable) + static const bool _developmentMode = true; /// POST request /// @@ -34,6 +36,30 @@ class ApiClient { .timeout(_timeout); } catch (e) { if (kDebugMode) debugPrint('ApiClient.post network error: $e'); + + // Development mode: return mock responses for common endpoints on network errors + if (_developmentMode) { + if (url.contains('/user/login/')) { + if (kDebugMode) debugPrint('Development mode: returning mock login response'); + final email = finalBody['username'] ?? 'test@example.com'; + return { + 'token': 'mock_token_${DateTime.now().millisecondsSinceEpoch}', + 'username': email, + 'email': email, + 'phone_number': '+1234567890', + }; + } else if (url.contains('/user/register/')) { + if (kDebugMode) debugPrint('Development mode: returning mock register response'); + final email = finalBody['email'] ?? 'test@example.com'; + return { + 'token': 'mock_token_${DateTime.now().millisecondsSinceEpoch}', + 'username': email, + 'email': email, + 'phone_number': finalBody['phone_number'] ?? '+1234567890', + }; + } + } + throw Exception('Network error: $e'); } diff --git a/lib/core/theme_manager.dart b/lib/core/theme_manager.dart index 7993940..45fc1ee 100644 --- a/lib/core/theme_manager.dart +++ b/lib/core/theme_manager.dart @@ -10,9 +10,15 @@ class ThemeManager { /// Call during app startup to load saved preference. static Future init() async { - final prefs = await SharedPreferences.getInstance(); - final isDark = prefs.getBool(_prefKey) ?? false; - themeMode.value = isDark ? ThemeMode.dark : ThemeMode.light; + try { + final prefs = await SharedPreferences.getInstance(); + final isDark = prefs.getBool(_prefKey) ?? false; + themeMode.value = isDark ? ThemeMode.dark : ThemeMode.light; + } catch (e) { + // If SharedPreferences fails, default to light theme + print('Error initializing theme: $e'); + themeMode.value = ThemeMode.light; + } } /// Set theme and persist diff --git a/lib/main.dart b/lib/main.dart index f088609..6cec356 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -39,9 +39,12 @@ class MyApp extends StatelessWidget { }, ); + static const String _fontFamily = 'Gilroy'; + ThemeData _lightTheme() { return ThemeData( brightness: Brightness.light, + fontFamily: _fontFamily, primarySwatch: primarySwatch, scaffoldBackgroundColor: const Color(0xFFF7F5FB), appBarTheme: const AppBarTheme( @@ -61,9 +64,9 @@ class MyApp extends StatelessWidget { } ThemeData _darkTheme() { - // Basic dark theme based on your sample — tweak colors as desired return ThemeData( brightness: Brightness.dark, + fontFamily: _fontFamily, primarySwatch: primarySwatch, scaffoldBackgroundColor: const Color(0xFF0B1220), appBarTheme: const AppBarTheme( @@ -75,7 +78,7 @@ class MyApp extends StatelessWidget { style: ElevatedButton.styleFrom(backgroundColor: const Color(0xFF0B63D6)), ), cardColor: const Color(0xFF0E1620), - textTheme: ThemeData.dark().textTheme, + textTheme: ThemeData.dark().textTheme.apply(fontFamily: _fontFamily), useMaterial3: false, ); } @@ -115,9 +118,17 @@ class _StartupScreenState extends State { } Future _loadLoginState() async { - final prefs = await SharedPreferences.getInstance(); - final hasEmail = prefs.getString('email') != null && prefs.getString('email')!.isNotEmpty; - setState(() => _loggedIn = hasEmail); + try { + final prefs = await SharedPreferences.getInstance(); + final hasEmail = prefs.getString('email') != null && prefs.getString('email')!.isNotEmpty; + setState(() => _loggedIn = hasEmail); + } catch (e) { + // If SharedPreferences fails (common on web with plugin issues), default to not logged in + print('Error loading login state: $e'); + if (mounted) { + setState(() => _loggedIn = false); + } + } } @override diff --git a/lib/screens/desktop_login_screen.dart b/lib/screens/desktop_login_screen.dart index 08dccab..d050d0a 100644 --- a/lib/screens/desktop_login_screen.dart +++ b/lib/screens/desktop_login_screen.dart @@ -237,8 +237,8 @@ class _DesktopLoginScreenState extends State with SingleTick ), ), const SizedBox(height: 12), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + Wrap( + alignment: WrapAlignment.spaceBetween, children: [ TextButton(onPressed: _openRegister, child: const Text("Don't have an account? Register")), TextButton(onPressed: () {}, child: const Text('Contact support')) diff --git a/lib/screens/learn_more_screen.dart b/lib/screens/learn_more_screen.dart index 13ef221..52b24ea 100644 --- a/lib/screens/learn_more_screen.dart +++ b/lib/screens/learn_more_screen.dart @@ -1,6 +1,7 @@ // lib/screens/learn_more_screen.dart import 'dart:async'; import 'dart:ui'; +import 'package:flutter/foundation.dart' show kIsWeb; import 'package:flutter/material.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart'; import 'package:share_plus/share_plus.dart'; @@ -222,107 +223,125 @@ class _LearnMoreScreenState extends State { } final screenHeight = MediaQuery.of(context).size.height; - final imageHeight = screenHeight * 0.50; - final overlap = 30.0; + final imageHeight = screenHeight * 0.45; + final topPadding = MediaQuery.of(context).padding.top; return Scaffold( backgroundColor: theme.scaffoldBackgroundColor, body: Stack( children: [ - // ── LAYER 1: Image carousel (background) ── - _buildImageCarousel(theme, imageHeight), - - // ── LAYER 2: Scrollable content with overlapping white card ── + // ── Scrollable content (carousel + card scroll together) ── SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Transparent spacer — shows the image behind - SizedBox(height: imageHeight - overlap), + // Image carousel (scrolls with content) + _buildImageCarousel(theme, imageHeight), - // White card with rounded top corners overlapping image - Container( - width: double.infinity, - decoration: BoxDecoration( - color: theme.scaffoldBackgroundColor, - borderRadius: const BorderRadius.vertical( - top: Radius.circular(28), - ), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.08), - blurRadius: 20, - offset: const Offset(0, -6), + // Content card with rounded top corners overlapping carousel + Transform.translate( + offset: const Offset(0, -28), + child: Container( + width: double.infinity, + decoration: BoxDecoration( + color: theme.scaffoldBackgroundColor, + borderRadius: const BorderRadius.vertical( + top: Radius.circular(28), ), - ], - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _buildTitleSection(theme), - _buildAboutSection(theme), - if (_event!.latitude != null && _event!.longitude != null) ...[ - _buildVenueSection(theme), - _buildGetDirectionsButton(theme), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.08), + blurRadius: 20, + offset: const Offset(0, -6), + ), ], - if (_event!.importantInfo.isNotEmpty) - _buildImportantInfoSection(theme), - if (_event!.importantInfo.isEmpty && - (_event!.importantInformation ?? '').isNotEmpty) - _buildImportantInfoFallback(theme), - const SizedBox(height: 100), - ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildTitleSection(theme), + _buildAboutSection(theme), + if (_event!.latitude != null && _event!.longitude != null) ...[ + _buildVenueSection(theme), + _buildGetDirectionsButton(theme), + ], + if (_event!.importantInfo.isNotEmpty) + _buildImportantInfoSection(theme), + if (_event!.importantInfo.isEmpty && + (_event!.importantInformation ?? '').isNotEmpty) + _buildImportantInfoFallback(theme), + const SizedBox(height: 100), + ], + ), ), ), ], ), ), - // ── LAYER 3: Floating icon row (above scrollview so taps work) ── + // ── Fixed top bar with back/share/heart buttons ── Positioned( - top: MediaQuery.of(context).padding.top + 10, - left: 16, - right: 16, - child: Row( - children: [ - _squareIconButton( - icon: Icons.arrow_back, - onTap: () => Navigator.pop(context), + top: 0, + left: 0, + right: 0, + child: Container( + padding: EdgeInsets.only( + top: topPadding + 10, + bottom: 10, + left: 16, + right: 16, + ), + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Colors.black.withOpacity(0.5), + Colors.black.withOpacity(0.0), + ], ), - // Pill-shaped page indicators (centered) - Expanded( - child: _imageUrls.length > 1 - ? Row( - mainAxisAlignment: MainAxisAlignment.center, - children: List.generate(_imageUrls.length, (i) { - final active = i == _currentPage; - return AnimatedContainer( - duration: const Duration(milliseconds: 300), - margin: const EdgeInsets.symmetric(horizontal: 3), - width: active ? 18 : 8, - height: 6, - decoration: BoxDecoration( - color: active - ? Colors.white - : Colors.white.withOpacity(0.45), - borderRadius: BorderRadius.circular(3), - ), - ); - }), - ) - : const SizedBox.shrink(), - ), - _squareIconButton( - icon: Icons.ios_share_outlined, - onTap: _shareEvent, - ), - const SizedBox(width: 10), - _squareIconButton( - icon: _wishlisted ? Icons.favorite : Icons.favorite_border, - iconColor: _wishlisted ? Colors.redAccent : Colors.white, - onTap: () => setState(() => _wishlisted = !_wishlisted), - ), - ], + ), + child: Row( + children: [ + _squareIconButton( + icon: Icons.arrow_back, + onTap: () => Navigator.pop(context), + ), + // Pill-shaped page indicators (centered) + Expanded( + child: _imageUrls.length > 1 + ? Row( + mainAxisAlignment: MainAxisAlignment.center, + children: List.generate(_imageUrls.length, (i) { + final active = i == _currentPage; + return AnimatedContainer( + duration: const Duration(milliseconds: 300), + margin: const EdgeInsets.symmetric(horizontal: 3), + width: active ? 18 : 8, + height: 6, + decoration: BoxDecoration( + color: active + ? Colors.white + : Colors.white.withOpacity(0.45), + borderRadius: BorderRadius.circular(3), + ), + ); + }), + ) + : const SizedBox.shrink(), + ), + _squareIconButton( + icon: Icons.ios_share_outlined, + onTap: _shareEvent, + ), + const SizedBox(width: 10), + _squareIconButton( + icon: _wishlisted ? Icons.favorite : Icons.favorite_border, + iconColor: _wishlisted ? Colors.redAccent : Colors.white, + onTap: () => setState(() => _wishlisted = !_wishlisted), + ), + ], + ), ), ), ], @@ -492,7 +511,7 @@ class _LearnMoreScreenState extends State { ); } - /// Square icon button with rounded corners and translucent white background + /// Square icon button with rounded corners and prominent background Widget _squareIconButton({ required IconData icon, required VoidCallback onTap, @@ -501,12 +520,12 @@ class _LearnMoreScreenState extends State { return GestureDetector( onTap: onTap, child: Container( - width: 42, - height: 42, + width: 44, + height: 44, decoration: BoxDecoration( - color: Colors.white.withOpacity(0.2), + color: Colors.black.withOpacity(0.35), borderRadius: BorderRadius.circular(12), - border: Border.all(color: Colors.white.withOpacity(0.3)), + border: Border.all(color: Colors.white.withOpacity(0.4)), ), child: Icon(icon, color: iconColor, size: 22), ), @@ -640,26 +659,66 @@ class _LearnMoreScreenState extends State { height: 280, child: Stack( children: [ - GoogleMap( - initialCameraPosition: CameraPosition( - target: LatLng(lat, lng), - zoom: 15, - ), - mapType: _mapType, - markers: { - Marker( - markerId: const MarkerId('event'), - position: LatLng(lat, lng), - infoWindow: InfoWindow(title: venueLabel), + // Use static map image on web (Google Maps JS SDK not configured), + // native GoogleMap widget on mobile + if (kIsWeb) + GestureDetector( + onTap: _viewLargerMap, + child: Container( + decoration: BoxDecoration( + color: Colors.grey.shade200, + borderRadius: BorderRadius.circular(20), + ), + child: Stack( + children: [ + Positioned.fill( + child: Image.network( + 'https://maps.googleapis.com/maps/api/staticmap?center=$lat,$lng&zoom=15&size=600x300&markers=color:red%7C$lat,$lng&key=', + fit: BoxFit.cover, + errorBuilder: (_, __, ___) => Container( + color: const Color(0xFFE8EAF6), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.map_outlined, size: 48, color: theme.colorScheme.primary), + const SizedBox(height: 8), + Text( + 'Tap to view on Google Maps', + style: TextStyle( + color: theme.colorScheme.primary, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), + ), + ), + ], + ), ), - }, - myLocationButtonEnabled: false, - zoomControlsEnabled: false, - scrollGesturesEnabled: true, - rotateGesturesEnabled: false, - tiltGesturesEnabled: false, - onMapCreated: (c) => _mapController = c, - ), + ) + else + GoogleMap( + initialCameraPosition: CameraPosition( + target: LatLng(lat, lng), + zoom: 15, + ), + mapType: _mapType, + markers: { + Marker( + markerId: const MarkerId('event'), + position: LatLng(lat, lng), + infoWindow: InfoWindow(title: venueLabel), + ), + }, + myLocationButtonEnabled: false, + zoomControlsEnabled: false, + scrollGesturesEnabled: true, + rotateGesturesEnabled: false, + tiltGesturesEnabled: false, + onMapCreated: (c) => _mapController = c, + ), // "View larger map" – top left Positioned( @@ -691,36 +750,38 @@ class _LearnMoreScreenState extends State { ), ), - // Map type toggle – bottom left - Positioned( - bottom: 12, - left: 12, - child: _mapControlButton( - icon: _mapType == MapType.normal - ? Icons.satellite_alt - : Icons.map_outlined, - onTap: () { - setState(() { - _mapType = _mapType == MapType.normal - ? MapType.satellite - : MapType.normal; - }); - }, + // Map type toggle – bottom left (native only) + if (!kIsWeb) + Positioned( + bottom: 12, + left: 12, + child: _mapControlButton( + icon: _mapType == MapType.normal + ? Icons.satellite_alt + : Icons.map_outlined, + onTap: () { + setState(() { + _mapType = _mapType == MapType.normal + ? MapType.satellite + : MapType.normal; + }); + }, + ), ), - ), - // Map controls toggle – bottom right - Positioned( - bottom: 12, - right: 12, - child: _mapControlButton( - icon: Icons.open_with_rounded, - onTap: () => setState(() => _showMapControls = !_showMapControls), + // Map controls toggle – bottom right (native only) + if (!kIsWeb) + Positioned( + bottom: 12, + right: 12, + child: _mapControlButton( + icon: Icons.open_with_rounded, + onTap: () => setState(() => _showMapControls = !_showMapControls), + ), ), - ), - // Directional pad overlay - if (_showMapControls) + // Directional pad overlay (native only) + if (!kIsWeb && _showMapControls) Positioned.fill( child: Container( decoration: BoxDecoration( @@ -730,7 +791,6 @@ class _LearnMoreScreenState extends State { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - // Top row: Up + Zoom In Row( mainAxisAlignment: MainAxisAlignment.center, children: [ @@ -744,7 +804,6 @@ class _LearnMoreScreenState extends State { ], ), const SizedBox(height: 10), - // Middle row: Left + Right Row( mainAxisAlignment: MainAxisAlignment.center, children: [ @@ -758,7 +817,6 @@ class _LearnMoreScreenState extends State { ], ), const SizedBox(height: 10), - // Bottom row: Down + Zoom Out + Close Row( mainAxisAlignment: MainAxisAlignment.center, children: [ diff --git a/lib/screens/login_screen.dart b/lib/screens/login_screen.dart index acd724e..d89507e 100644 --- a/lib/screens/login_screen.dart +++ b/lib/screens/login_screen.dart @@ -1,9 +1,11 @@ // 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 'home_screen.dart'; -import '../core/app_decoration.dart'; class LoginScreen extends StatefulWidget { const LoginScreen({Key? key}) : super(key: key); @@ -22,9 +24,42 @@ class _LoginScreenState extends State { 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 _initVideo() async { + _videoController = VideoPlayerController.networkUrl( + Uri.parse('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(); @@ -35,7 +70,6 @@ class _LoginScreenState extends State { String? _emailValidator(String? v) { if (v == null || v.trim().isEmpty) return 'Enter email'; final email = v.trim(); - // Basic email pattern check final emailRegex = RegExp(r"^[^@]+@[^@]+\.[^@]+"); if (!emailRegex.hasMatch(email)) return 'Enter a valid email'; return null; @@ -58,11 +92,9 @@ class _LoginScreenState extends State { setState(() => _loading = true); try { - // AuthService.login now returns a UserModel and also persists profile info. await _auth.login(email, password); if (!mounted) return; - // small delay for UX await Future.delayed(const Duration(milliseconds: 150)); Navigator.of(context).pushReplacement(PageRouteBuilder( @@ -86,149 +118,410 @@ class _LoginScreenState extends State { 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) { - const primary = Color(0xFF0B63D6); - final width = MediaQuery.of(context).size.width; - return Scaffold( - // backgroundColor: primary, - body: Container( - decoration: AppDecoration.blueGradient, - child: SafeArea( - child: Center( - child: SingleChildScrollView( - padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 18), - child: ConstrainedBox( - constraints: BoxConstraints(maxWidth: width < 720 ? width : 720), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - const SizedBox(height: 6), - const Text('Welcome', style: TextStyle(fontSize: 34, fontWeight: FontWeight.bold, color: Colors.white)), - const SizedBox(height: 6), - const Text('Sign in to continue', style: TextStyle(color: Colors.white70, fontSize: 16)), - const SizedBox(height: 26), + 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), + ), + ), + ), - // HERO card - Hero( - tag: 'headerCard', - flightShuttleBuilder: (flightContext, animation, flightDirection, fromContext, toContext) { - return Material( - color: Colors.transparent, - child: ScaleTransition( - scale: animation.drive(Tween(begin: 0.98, end: 1.0).chain(CurveTween(curve: Curves.easeOut))), - child: fromContext.widget, + // 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, + ), + ), ), - ); - }, - child: Material( - type: MaterialType.transparency, - child: Container( - width: double.infinity, - decoration: AppDecoration.blueGradientRounded(20).copyWith( - boxShadow: const [BoxShadow(color: Colors.black26, blurRadius: 18, offset: Offset(0, 8))], + 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, + ), + ), ), - padding: const EdgeInsets.fromLTRB(18, 18, 18, 18), - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, + 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: [ - const SizedBox(height: 8), - const Text('Eventify', style: TextStyle(color: Colors.white, fontSize: 22, fontWeight: FontWeight.bold)), - const SizedBox(height: 12), - - // white card inside the blue card — now uses Form - Form( - key: _formKey, - child: Container( - decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(12)), - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 14), - child: Column( - children: [ - // Email - TextFormField( - controller: _emailCtrl, - focusNode: _emailFocus, - keyboardType: TextInputType.emailAddress, - autofillHints: const [AutofillHints.email], - decoration: InputDecoration( - labelText: 'Email', - border: InputBorder.none, - prefixIcon: Icon(Icons.email, color: primary), - ), - validator: _emailValidator, - textInputAction: TextInputAction.next, - onFieldSubmitted: (_) { - FocusScope.of(context).requestFocus(_passFocus); - }, + // 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, ), - const Divider(), - // Password - TextFormField( - controller: _passCtrl, - focusNode: _passFocus, - obscureText: true, - decoration: InputDecoration( - labelText: 'Password', - border: InputBorder.none, - prefixIcon: Icon(Icons.lock, color: primary), - ), - validator: _passwordValidator, - textInputAction: TextInputAction.done, - onFieldSubmitted: (_) => _performLogin(), - ), - ], - ), + 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), + ), + ], ), ), - - const SizedBox(height: 18), - - // Login button - SizedBox( - width: double.infinity, - child: ElevatedButton( - onPressed: _loading ? null : _performLogin, - style: ElevatedButton.styleFrom( - backgroundColor: Colors.white, - foregroundColor: primary, - padding: const EdgeInsets.symmetric(vertical: 14), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), - ), - child: _loading - ? SizedBox(height: 18, width: 18, child: CircularProgressIndicator(strokeWidth: 2, color: primary)) - : const Text('Login', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600)), + // Forgot Password + GestureDetector( + onTap: _showComingSoon, + child: const Text( + 'Forgot Password?', + style: TextStyle(color: _textMuted, fontSize: 12), ), ), - - const SizedBox(height: 8), ], ), - ), - ), - ), + const SizedBox(height: 24), - const SizedBox(height: 18), - Center( - child: Column( - children: [ - const SizedBox(height: 8), - TextButton( - onPressed: _openRegister, - child: const Text("Don't have an account? Register", style: TextStyle(color: Colors.white)), + // 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, + ), + ), + ), + ], + ), ), ], ), ), - ], + ), ), ), ), - ), + ], ), - ), - ); -} + ); + } } /// Register screen calls backend register endpoint via AuthService.register