diff --git a/assets/login/app_icon.png b/assets/login/app_icon.png new file mode 100644 index 0000000..702f709 Binary files /dev/null and b/assets/login/app_icon.png differ diff --git a/assets/login/bg.png b/assets/login/bg.png new file mode 100644 index 0000000..1d8316c Binary files /dev/null and b/assets/login/bg.png differ diff --git a/assets/onboarding/bg1.jpg b/assets/onboarding/bg1.jpg new file mode 100644 index 0000000..b46db47 Binary files /dev/null and b/assets/onboarding/bg1.jpg differ diff --git a/assets/onboarding/bg2.png b/assets/onboarding/bg2.png new file mode 100644 index 0000000..4705e7b Binary files /dev/null and b/assets/onboarding/bg2.png differ diff --git a/ios/Flutter/Debug.xcconfig b/ios/Flutter/Debug.xcconfig index 592ceee..ec97fc6 100644 --- a/ios/Flutter/Debug.xcconfig +++ b/ios/Flutter/Debug.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "Generated.xcconfig" diff --git a/ios/Flutter/Release.xcconfig b/ios/Flutter/Release.xcconfig index 592ceee..c4855bf 100644 --- a/ios/Flutter/Release.xcconfig +++ b/ios/Flutter/Release.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "Generated.xcconfig" diff --git a/ios/Podfile b/ios/Podfile new file mode 100644 index 0000000..620e46e --- /dev/null +++ b/ios/Podfile @@ -0,0 +1,43 @@ +# Uncomment this line to define a global platform for your project +# platform :ios, '13.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + use_frameworks! + + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + end +end diff --git a/lib/core/app_router.dart b/lib/core/app_router.dart index f3f8daa..ef2b430 100644 --- a/lib/core/app_router.dart +++ b/lib/core/app_router.dart @@ -1,16 +1,44 @@ import 'package:flutter/material.dart'; - +import '../login/views/forgot_password_page.dart'; +import '../login/views/login_page.dart'; +import '../login/views/otp_verification_page.dart'; +import '../login/views/reset_password_page.dart'; +import '../onboarding/views/onboarding_page.dart'; +import '../profile/views/profile_page.dart'; +import '../scan_history/views/scan_history_page.dart'; +// Import other screens here as needed class AppRouter { + static const String onboarding = '/onboarding'; + static const String login = '/login'; + static const String home = '/home'; + static const String scanHistory = '/scanHistory'; + static const String forgotPassword = '/forgot_password'; + static const String otpVerification = '/otp_verification'; + static const String resetPassword = '/reset_password'; + static const String profileScreen = '/profile_screen'; + static Route generateRoute(RouteSettings settings) { switch (settings.name) { - case '/login': + case onboarding: + return MaterialPageRoute(builder: (_) => const OnboardingPage()); + case login: return MaterialPageRoute(builder: (_) => const LoginPage()); - case '/home': - return MaterialPageRoute(builder: (_) => const HomePage()); + case scanHistory: + return MaterialPageRoute(builder: (_) => const ScanHistoryPage()); + case forgotPassword: + return MaterialPageRoute(builder: (_) => const ForgotPasswordPage()); + case otpVerification: + return MaterialPageRoute(builder: (_) => const OtpVerificationPage()); + case resetPassword: + return MaterialPageRoute(builder: (_) => const ResetPasswordPage()); + case profileScreen: + return MaterialPageRoute(builder: (_) => const ProfileScreen()); default: return MaterialPageRoute( - builder: (_) => const Scaffold(body: Center(child: Text("Page not found")))); + builder: (_) => + const Scaffold(body: Center(child: Text('No route defined'))), + ); } } } diff --git a/lib/login/blocs/forgot_password_bloc.dart b/lib/login/blocs/forgot_password_bloc.dart new file mode 100644 index 0000000..7d1e848 --- /dev/null +++ b/lib/login/blocs/forgot_password_bloc.dart @@ -0,0 +1,82 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; + +// ---------------- EVENTS ---------------- +abstract class ForgotPasswordEvent {} + +class EmailChanged extends ForgotPasswordEvent { + final String email; + EmailChanged(this.email); +} + +class SendResetLink extends ForgotPasswordEvent {} + +// ---------------- STATE ---------------- +class ForgotPasswordState { + final String email; + final bool isValidEmail; + final bool isLoading; + final bool isSuccess; + final String message; + + const ForgotPasswordState({ + required this.email, + required this.isValidEmail, + required this.isLoading, + required this.isSuccess, + required this.message, + }); + + ForgotPasswordState copyWith({ + String? email, + bool? isValidEmail, + bool? isLoading, + bool? isSuccess, + String? message, + }) { + return ForgotPasswordState( + email: email ?? this.email, + isValidEmail: isValidEmail ?? this.isValidEmail, + isLoading: isLoading ?? this.isLoading, + isSuccess: isSuccess ?? this.isSuccess, + message: message ?? this.message, + ); + } + + factory ForgotPasswordState.initial() => const ForgotPasswordState( + email: '', + isValidEmail: false, + isLoading: false, + isSuccess: false, + message: '', + ); +} + +// ---------------- BLOC ---------------- +class ForgotPasswordBloc + extends Bloc { + ForgotPasswordBloc() : super(ForgotPasswordState.initial()) { + // when email changes + on((event, emit) { + final isValid = _isValidEmail(event.email); + emit(state.copyWith(email: event.email, isValidEmail: isValid)); + }); + + // when button clicked + on((event, emit) async { + if (!state.isValidEmail) return; + + emit(state.copyWith(isLoading: true, message: '')); + await Future.delayed(const Duration(seconds: 2)); // simulate API delay + emit(state.copyWith( + isLoading: false, + isSuccess: true, + message: 'Reset link sent successfully!', + )); + }); + } + + bool _isValidEmail(String email) { + final regex = RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$'); + return regex.hasMatch(email); + } +} diff --git a/lib/login/blocs/login_bloc.dart b/lib/login/blocs/login_bloc.dart new file mode 100644 index 0000000..e69de29 diff --git a/lib/login/blocs/otp_bloc.dart b/lib/login/blocs/otp_bloc.dart new file mode 100644 index 0000000..96cfcfd --- /dev/null +++ b/lib/login/blocs/otp_bloc.dart @@ -0,0 +1,103 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; + +// --------------------- EVENTS --------------------- +abstract class OtpEvent {} + +/// Triggered when user types or pastes OTP +class OtpChanged extends OtpEvent { + final String otp; + OtpChanged(this.otp); +} + +/// Triggered when user presses Verify button +class OtpVerify extends OtpEvent {} + +/// (Optional for later) Triggered when user taps "Resend OTP" +class OtpResend extends OtpEvent {} + + +// --------------------- STATE --------------------- +class OtpState { + final String otp; + final bool isVerified; + final bool isLoading; + final bool isResending; + final String message; + + /// Computed property to know if OTP input is complete (6 digits) + bool get isOtpFilled => otp.length == 6; + + const OtpState({ + required this.otp, + required this.isVerified, + required this.isLoading, + required this.isResending, + required this.message, + }); + + OtpState copyWith({ + String? otp, + bool? isVerified, + bool? isLoading, + bool? isResending, + String? message, + }) { + return OtpState( + otp: otp ?? this.otp, + isVerified: isVerified ?? this.isVerified, + isLoading: isLoading ?? this.isLoading, + isResending: isResending ?? this.isResending, + message: message ?? this.message, + ); + } + + factory OtpState.initial() => const OtpState( + otp: '', + isVerified: false, + isLoading: false, + isResending: false, + message: '', + ); +} + + +// --------------------- BLOC --------------------- +class OtpBloc extends Bloc { + OtpBloc() : super(OtpState.initial()) { + // Handle typing input + on((event, emit) { + emit(state.copyWith(otp: event.otp, message: '')); + }); + + // Handle Verify + on((event, emit) async { + if (!state.isOtpFilled) return; // no action if OTP incomplete + + emit(state.copyWith(isLoading: true, message: '')); + + await Future.delayed(const Duration(seconds: 2)); // simulate API delay + + // Mock success condition — replace with API later + if (state.otp == "123456") { + emit(state.copyWith( + isVerified: true, + isLoading: false, + message: "OTP verified successfully!", + )); + } else { + emit(state.copyWith( + isVerified: false, + isLoading: false, + message: "Invalid OTP. Please try again.", + )); + } + }); + + // Handle Resend OTP (optional for later) + on((event, emit) async { + emit(state.copyWith(isResending: true, message: 'Resending OTP...')); + await Future.delayed(const Duration(seconds: 3)); // simulate resend + emit(state.copyWith(isResending: false, message: 'OTP resent successfully!')); + }); + } +} diff --git a/lib/login/blocs/reset_password_bloc.dart b/lib/login/blocs/reset_password_bloc.dart new file mode 100644 index 0000000..f206d8e --- /dev/null +++ b/lib/login/blocs/reset_password_bloc.dart @@ -0,0 +1,93 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; + +/// EVENTS +abstract class ResetPasswordEvent {} + +class PasswordChanged extends ResetPasswordEvent { + final String password; + PasswordChanged(this.password); +} + +class ConfirmPasswordChanged extends ResetPasswordEvent { + final String confirmPassword; + ConfirmPasswordChanged(this.confirmPassword); +} + +/// STATE +class ResetPasswordState { + final String password; + final String confirmPassword; + final bool hasMinLength; + final bool hasUppercase; + final bool hasNumber; + final int strengthLevel; // 0–4 based on password strength + + const ResetPasswordState({ + required this.password, + required this.confirmPassword, + required this.hasMinLength, + required this.hasUppercase, + required this.hasNumber, + required this.strengthLevel, + }); + + factory ResetPasswordState.initial() => const ResetPasswordState( + password: '', + confirmPassword: '', + hasMinLength: false, + hasUppercase: false, + hasNumber: false, + strengthLevel: 0, + ); + + ResetPasswordState copyWith({ + String? password, + String? confirmPassword, + bool? hasMinLength, + bool? hasUppercase, + bool? hasNumber, + int? strengthLevel, + }) { + return ResetPasswordState( + password: password ?? this.password, + confirmPassword: confirmPassword ?? this.confirmPassword, + hasMinLength: hasMinLength ?? this.hasMinLength, + hasUppercase: hasUppercase ?? this.hasUppercase, + hasNumber: hasNumber ?? this.hasNumber, + strengthLevel: strengthLevel ?? this.strengthLevel, + ); + } +} + +/// BLOC +class ResetPasswordBloc extends Bloc { + ResetPasswordBloc() : super(ResetPasswordState.initial()) { + on((event, emit) { + final pwd = event.password; + final hasMin = pwd.length >= 8; + final hasUp = pwd.contains(RegExp(r'[A-Z]')); + final hasNum = pwd.contains(RegExp(r'[0-9]')); + + // count how many validations passed (max 3) + int passed = [hasMin, hasUp, hasNum].where((e) => e).length; + + // convert to 4-box level (0–4) + int level = 0; + if (passed == 1) level = 1; + if (passed == 2) level = 2; + if (passed == 3) level = 4; // all conditions passed => full bar + + emit(state.copyWith( + password: pwd, + hasMinLength: hasMin, + hasUppercase: hasUp, + hasNumber: hasNum, + strengthLevel: level, + )); + }); + + on((event, emit) { + emit(state.copyWith(confirmPassword: event.confirmPassword)); + }); + } +} diff --git a/lib/login/models/login.dart b/lib/login/models/login.dart new file mode 100644 index 0000000..e69de29 diff --git a/lib/login/repositories/forgot_password_repository.dart b/lib/login/repositories/forgot_password_repository.dart new file mode 100644 index 0000000..e69de29 diff --git a/lib/login/repositories/login_repository.dart b/lib/login/repositories/login_repository.dart new file mode 100644 index 0000000..e69de29 diff --git a/lib/login/repositories/otp_repository.dart b/lib/login/repositories/otp_repository.dart new file mode 100644 index 0000000..e69de29 diff --git a/lib/login/repositories/reset_password_repository.dart b/lib/login/repositories/reset_password_repository.dart new file mode 100644 index 0000000..e69de29 diff --git a/lib/login/viewmodels/forgot_password_viewmodel.dart b/lib/login/viewmodels/forgot_password_viewmodel.dart new file mode 100644 index 0000000..e69de29 diff --git a/lib/login/viewmodels/login_viewmodel.dart b/lib/login/viewmodels/login_viewmodel.dart new file mode 100644 index 0000000..e69de29 diff --git a/lib/login/viewmodels/otp_viewmodel.dart b/lib/login/viewmodels/otp_viewmodel.dart new file mode 100644 index 0000000..e69de29 diff --git a/lib/login/viewmodels/reset_password_viewmodel.dart b/lib/login/viewmodels/reset_password_viewmodel.dart new file mode 100644 index 0000000..e69de29 diff --git a/lib/login/views/forgot_password_page.dart b/lib/login/views/forgot_password_page.dart new file mode 100644 index 0000000..f83b640 --- /dev/null +++ b/lib/login/views/forgot_password_page.dart @@ -0,0 +1,204 @@ +import 'dart:ui'; +import 'package:citycards_partner_flutter/core/app_router.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:google_fonts/google_fonts.dart'; +import '../blocs/forgot_password_bloc.dart'; + +class ForgotPasswordPage extends StatelessWidget { + const ForgotPasswordPage({super.key}); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (_) => ForgotPasswordBloc(), + child: const _ForgotPasswordView(), + ); + } +} + +class _ForgotPasswordView extends StatelessWidget { + const _ForgotPasswordView(); + + @override + Widget build(BuildContext context) { + final bloc = context.read(); + + return Scaffold( + body: Stack( + children: [ + // Background + Positioned.fill( + child: Image.asset( + 'assets/login/bg.png', + fit: BoxFit.cover, + ), + ), + + // Gradient Overlay + Positioned.fill( + child: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Colors.black.withOpacity(0.3), + Colors.black.withOpacity(0.6), + Colors.black.withOpacity(1.0), + ], + ), + ), + ), + ), + + // Foreground content + Center( + child: SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: BlocConsumer( + listener: (context, state) { + if (state.isSuccess) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text("Reset link sent successfully!"), + backgroundColor: Colors.green, + ), + ); + Navigator.pushNamed(context, AppRouter.otpVerification); + } else if (state.message.isNotEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(state.message), + backgroundColor: Colors.redAccent, + ), + ); + } + }, + builder: (context, state) { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const SizedBox(height: 160), + + // Glass Card + ClipRRect( + borderRadius: BorderRadius.circular(20), + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 5, sigmaY: 5), + child: Container( + padding: const EdgeInsets.all(24), + decoration: BoxDecoration( + color: Colors.black.withOpacity(0.5), + borderRadius: BorderRadius.circular(20), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Align( + alignment: Alignment.center, + child: Image.asset( + "assets/login/app_icon.png", + scale: 4, + ), + ), + const SizedBox(height: 24), + Text( + "Forgot Password", + style: GoogleFonts.poppins( + color: Colors.white, + fontSize: 20, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 8), + Text( + "Forgot your password? Don’t worry — just enter your email and we’ll help you reset it.", + textAlign: TextAlign.start, + style: GoogleFonts.poppins( + color: Colors.white, + fontSize: 13, + ), + ), + const SizedBox(height: 24), + + // Email Field + TextField( + style: const TextStyle(color: Colors.white), + onChanged: (value) => + bloc.add(EmailChanged(value)), + decoration: InputDecoration( + prefixIcon: const Icon( + Icons.email_outlined, + color: Colors.white70, + ), + hintText: 'Enter your email address', + hintStyle: const TextStyle( + color: Colors.white54), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: const BorderSide( + color: Colors.white54), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: const BorderSide( + color: Colors.white), + ), + ), + ), + const SizedBox(height: 16), + ], + ), + ), + ), + ), + + const SizedBox(height: 148), + + SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: state.isValidEmail && !state.isLoading + ? () => bloc.add(SendResetLink()) + : null, + style: ButtonStyle( + backgroundColor: + MaterialStateProperty.resolveWith( + (states) { + if (states.contains(MaterialState.disabled)) { + return const Color(0xFF9C3F42); + } + return const Color(0xFFFF4C4C); + }, + ), + padding: MaterialStateProperty.all( + const EdgeInsets.symmetric(vertical: 14), + ), + shape: MaterialStateProperty.all( + RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + ), + child: Text( + "Send Reset Link", + style: GoogleFonts.poppins( + color: state.isValidEmail?Colors.white:Color(0xff9D9F9F), + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + ], + ); + }, + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/login/views/login_page.dart b/lib/login/views/login_page.dart new file mode 100644 index 0000000..34df939 --- /dev/null +++ b/lib/login/views/login_page.dart @@ -0,0 +1,238 @@ +import 'dart:ui'; + +import 'package:citycards_partner_flutter/core/app_router.dart'; +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; + +class LoginPage extends StatefulWidget { + const LoginPage({super.key}); + + @override + State createState() => _LoginPageState(); +} + +class _LoginPageState extends State { + bool _isPasswordVisible = false; + bool _rememberMe = false; + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Stack( + children: [ + // Background image + Positioned.fill( + child: Image.asset( + 'assets/login/bg.png', + fit: BoxFit.cover, + ), + ), + Positioned.fill( + child: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Colors.black.withOpacity(0.3), + Colors.black.withOpacity(0.6), + Colors.black.withOpacity(1.0), + ], + // stops: const [0.5, 1.0], + ), + ), + ), + ), + Center( + child: SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SizedBox(height: 80), + ClipRRect( + borderRadius: BorderRadius.circular(20), + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 5, sigmaY: 5), + child: Container( + padding: const EdgeInsets.all(24), + decoration: BoxDecoration( + color: Colors.black.withOpacity(0.5), + borderRadius: BorderRadius.circular(20), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Align( + alignment: AlignmentGeometry.center, + child: Image.asset("assets/login/app_icon.png",scale: 4,)), + const SizedBox(height: 24), + Text( + "Enter your email", + style: GoogleFonts.poppins( + color: Colors.white, + fontSize: 14, + ), + ), + const SizedBox(height: 8), + TextField( + style: const TextStyle(color: Colors.white), + decoration: InputDecoration( + hintText: '', + hintStyle: const TextStyle(color: Colors.white54), + prefixIcon: const Icon( + Icons.email_outlined, + color: Colors.white70, + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: + const BorderSide(color: Colors.white54), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: + const BorderSide(color: Colors.white), + ), + ), + ), + const SizedBox(height: 20), + + Text( + "Enter your password", + style: GoogleFonts.poppins( + color: Colors.white, + fontSize: 14, + ), + ), + const SizedBox(height: 8), + TextField( + obscureText: !_isPasswordVisible, + style: const TextStyle(color: Colors.white), + decoration: InputDecoration( + hintText: '', + hintStyle: const TextStyle(color: Colors.white54), + prefixIcon: const Icon( + Icons.lock_outline, + color: Colors.white70, + ), + suffixIcon: IconButton( + icon: Icon( + _isPasswordVisible + ? Icons.visibility_outlined + : Icons.visibility_off_outlined, + color: Colors.white70, + ), + onPressed: () { + setState(() { + _isPasswordVisible = !_isPasswordVisible; + }); + }, + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: + const BorderSide(color: Colors.white54), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: + const BorderSide(color: Colors.white), + ), + ), + ), + + const SizedBox(height: 12), + + // Remember me + Forgot password + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + GestureDetector( + onTap: () { + setState(() { + _rememberMe = !_rememberMe; + }); + }, + child: Container( + height: 18, + width: 18, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(3), + border: Border.all(color: Colors.white), + color: _rememberMe + ? Colors.white + : Colors.transparent, + ), + ), + ), + const SizedBox(width: 8), + Text( + "Remember me", + style: GoogleFonts.poppins( + color: Colors.white, + fontWeight: FontWeight.w400, + fontSize: 14, + ), + ), + ], + ), + GestureDetector( + onTap: () { + Navigator.pushNamed( + context, AppRouter.forgotPassword); + }, + child: Text( + "Forgot Password?", + style: GoogleFonts.poppins( + color: const Color(0xFFFF4C4C), + fontSize: 14, + fontWeight: FontWeight.w600, + ), + ), + ), + ], + ), + ], + ), + ), + ), + ), + + const SizedBox(height: 68), + + // Log in button + SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: () { + Navigator.pushNamed(context, '/home'); + }, + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFFFF4C4C), + padding: const EdgeInsets.symmetric(vertical: 14), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + ), + child: Text( + "Log in", + style: GoogleFonts.poppins( + color: Colors.white, + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), + ), + ), + ], + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/login/views/otp_verification_page.dart b/lib/login/views/otp_verification_page.dart new file mode 100644 index 0000000..786d2ba --- /dev/null +++ b/lib/login/views/otp_verification_page.dart @@ -0,0 +1,193 @@ +import 'dart:ui'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_otp_text_field/flutter_otp_text_field.dart'; +import 'package:google_fonts/google_fonts.dart'; +import '../blocs/otp_bloc.dart'; + +class OtpVerificationPage extends StatelessWidget { + const OtpVerificationPage({super.key}); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (_) => OtpBloc(), + child: Scaffold( + body: Stack( + children: [ + Positioned.fill( + child: Image.asset('assets/login/bg.png', fit: BoxFit.cover), + ), + Positioned.fill( + child: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Colors.black.withOpacity(0.3), + Colors.black.withOpacity(0.6), + Colors.black.withOpacity(1.0), + ], + ), + ), + ), + ), + + // Foreground content + Center( + child: SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 18), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const SizedBox(height: 80), + ClipRRect( + borderRadius: BorderRadius.circular(20), + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 5, sigmaY: 5), + child: Container( + padding: const EdgeInsets.all(24), + decoration: BoxDecoration( + color: Colors.black.withOpacity(0.4), + borderRadius: BorderRadius.circular(20), + border: Border.all( + color: Colors.white.withOpacity(0.2), + ), + ), + child: BlocConsumer( + listener: (context, state) { + if (state.isVerified) { + Navigator.pushNamed(context, '/reset_password'); + } else if (state.message.isNotEmpty && + !state.isLoading) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(state.message), + backgroundColor: state.isVerified + ? Colors.green + : Colors.redAccent, + ), + ); + } + }, + builder: (context, state) { + final otpBloc = context.read(); + return Column( + children: [ + Align( + alignment: Alignment.center, + child: Image.asset( + "assets/login/app_icon.png", + scale: 4, + ), + ), + const SizedBox(height: 24), + Text( + "Verify OTP", + style: GoogleFonts.poppins( + color: Colors.white, + fontSize: 24, + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(height: 8), + Text( + "We’ve sent an OTP to your registered email. Please enter it below, or use the reset link included in the email.", + textAlign: TextAlign.start, + style: GoogleFonts.poppins( + color: Colors.white, + fontSize: 15, + fontWeight: FontWeight.w400, + ), + ), + const SizedBox(height: 30), + + // OTP input fields + OtpTextField( + borderRadius: BorderRadius.all( + Radius.circular(6), + ), + numberOfFields: 6, + fillColor: Color(0xff242628), + cursorColor: Colors.white, + borderColor: Colors.white, + focusedBorderColor: const Color(0xFFFF4C4C), + showFieldAsBox: true, + fieldWidth: 45, + textStyle: const TextStyle( + color: Colors.white, + ), + onSubmit: (value) { + otpBloc.add(OtpChanged(value)); + otpBloc.add(OtpVerify()); + }, + onCodeChanged: (value) { + otpBloc.add(OtpChanged(value)); + }, + ), + const SizedBox(height: 24), + ], + ); + }, + ), + ), + ), + ), + const SizedBox(height: 60), + BlocBuilder( + builder: (context, state) { + final otpBloc = context.read(); + return SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: state.isOtpFilled && !state.isLoading + ? () => otpBloc.add(OtpVerify()) + : null, + style: ButtonStyle( + backgroundColor: + MaterialStateProperty.resolveWith(( + states, + ) { + if (states.contains( + MaterialState.disabled, + )) { + return const Color( + 0xFF9C3F42, + ); // 👈 custom disabled color + } + return const Color( + 0xFFFF4C4C, + ); // 👈 active color + }), + padding: MaterialStateProperty.all( + const EdgeInsets.symmetric(vertical: 14), + ), + shape: MaterialStateProperty.all( + RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + ), + child: Text( + "Verify", + style: GoogleFonts.poppins( + color: state.isOtpFilled?Colors.white:Color(0xff9D9F9F), + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + ), + ); + }, + ), + ], + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/login/views/reset_password_page.dart b/lib/login/views/reset_password_page.dart new file mode 100644 index 0000000..6f47579 --- /dev/null +++ b/lib/login/views/reset_password_page.dart @@ -0,0 +1,252 @@ +import 'dart:ui'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:google_fonts/google_fonts.dart'; +import '../../core/app_router.dart'; +import '../blocs/reset_password_bloc.dart'; + +class ResetPasswordPage extends StatelessWidget { + const ResetPasswordPage({super.key}); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (_) => ResetPasswordBloc(), + child: const _ResetPasswordView(), + ); + } +} + +class _ResetPasswordView extends StatelessWidget { + const _ResetPasswordView(); + + @override + Widget build(BuildContext context) { + final bloc = context.read(); + + return Scaffold( + body: Stack( + children: [ + Positioned.fill( + child: Image.asset('assets/login/bg.png', fit: BoxFit.cover), + ), + Positioned.fill( + child: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Colors.black.withOpacity(0.3), + Colors.black.withOpacity(0.6), + Colors.black.withOpacity(1.0), + ], + ), + ), + ), + ), + Center( + child: SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: BlocBuilder( + builder: (context, state) { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const SizedBox(height: 80), + ClipRRect( + borderRadius: BorderRadius.circular(20), + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10), + child: Container( + padding: const EdgeInsets.all(24), + decoration: BoxDecoration( + color: Colors.black.withOpacity(0.5), + borderRadius: BorderRadius.circular(20), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Align( + alignment: Alignment.center, + child: Image.asset( + "assets/login/app_icon.png", + scale: 4, + ), + ), + const SizedBox(height: 24), + Align( + alignment: Alignment.center, + child: Text( + "Reset your Password", + style: GoogleFonts.poppins( + color: Colors.white, + fontSize: 20, + fontWeight: FontWeight.w600, + ), + ), + ), + const SizedBox(height: 16), + + // ✅ Validation list + _buildValidationRow( + "Minimum of 8 characters", + state.hasMinLength), + _buildValidationRow( + "At least one uppercase letter (A–Z)", + state.hasUppercase), + _buildValidationRow( + "At least one number (0–9)", state.hasNumber), + const SizedBox(height: 20), + + // Password + TextField( + obscureText: true, + cursorColor: Colors.white, + onChanged: (value) => + bloc.add(PasswordChanged(value)), + style: const TextStyle(color: Colors.white), + decoration: InputDecoration( + prefixIcon: const Icon( + Icons.lock_outline, + color: Colors.white70, + ), + hintText: 'Enter your password', + hintStyle: + const TextStyle(color: Colors.white54), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: const BorderSide( + color: Colors.white54, + ), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: + const BorderSide(color: Colors.white), + ), + ), + ), + + // ✅ Strength boxes + const SizedBox(height: 10), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: List.generate(4, (i) { + final filled = i < state.strengthLevel; + return Expanded( + child: Container( + margin: EdgeInsets.only( + right: i < 3 ? 6 : 0), + height: 5, + decoration: BoxDecoration( + color: filled + ? const Color(0xFFFFA500) // orange + : Colors.white.withOpacity(0.2), + borderRadius: BorderRadius.circular(3), + ), + ), + ); + }), + ), + + const SizedBox(height: 20), + + // Confirm Password + TextField( + obscureText: true, + cursorColor: Colors.white, + onChanged: (value) => + bloc.add(ConfirmPasswordChanged(value)), + style: const TextStyle(color: Colors.white), + decoration: InputDecoration( + prefixIcon: const Icon( + Icons.lock_outline, + color: Colors.white70, + ), + hintText: 'Retype your password', + hintStyle: + const TextStyle(color: Colors.white54), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: const BorderSide( + color: Colors.white54, + ), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: + const BorderSide(color: Colors.white), + ), + ), + ), + const SizedBox(height: 30), + ], + ), + ), + ), + ), + const SizedBox(height: 60), + SizedBox( + height: 52, + width: double.infinity, + child: ElevatedButton( + onPressed: () { + Navigator.pushNamed( + context, AppRouter.otpVerification); + // Navigate or show success + }, + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFFFF4C4C), + padding: + const EdgeInsets.symmetric(vertical: 14), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + child: Text( + "Verify", + style: GoogleFonts.poppins( + color: Colors.white, + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), + ), + ), + ], + ); + }, + ), + ), + ), + ], + ), + ); + } + + // ✅ Validation circle + text + Widget _buildValidationRow(String text, bool isValid) { + return Padding( + padding: const EdgeInsets.only(bottom: 6), + child: Row( + children: [ + Text( + text, + style: GoogleFonts.poppins(color: Colors.white, fontSize: 13), + ), + const Spacer(), + Container( + height: 12, + width: 12, + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all(color: Colors.white), + color: isValid ? Colors.white : Colors.transparent, + ), + ), + ], + ), + ); + } +} diff --git a/lib/main.dart b/lib/main.dart index 6a5d8b8..79145c7 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,4 +1,9 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'core/app_router.dart'; + + void main() { WidgetsFlutterBinding.ensureInitialized(); @@ -12,17 +17,18 @@ void main() { class MyApp extends StatelessWidget { const MyApp({super.key}); - @override Widget build(BuildContext context) { return MaterialApp( title: 'City Cards Partner', + debugShowCheckedModeBanner: false, theme: ThemeData( textTheme: GoogleFonts.poppinsTextTheme( Theme.of(context).textTheme, ) ), - initialRoute: '/login', + initialRoute: AppRouter.profileScreen, + onGenerateRoute: AppRouter.generateRoute, ); } } diff --git a/lib/onboarding/blocs/onboarding_bloc.dart b/lib/onboarding/blocs/onboarding_bloc.dart new file mode 100644 index 0000000..e69de29 diff --git a/lib/onboarding/models/onboarding_model.dart b/lib/onboarding/models/onboarding_model.dart new file mode 100644 index 0000000..ba7aa93 --- /dev/null +++ b/lib/onboarding/models/onboarding_model.dart @@ -0,0 +1,11 @@ +class OnboardingModel { + final String image; + final String title; + final String description; + + OnboardingModel({ + required this.image, + required this.title, + required this.description, + }); +} diff --git a/lib/onboarding/repositories/onboarding_repository.dart b/lib/onboarding/repositories/onboarding_repository.dart new file mode 100644 index 0000000..e69de29 diff --git a/lib/onboarding/viewmodels/onboarding_viewmodel.dart b/lib/onboarding/viewmodels/onboarding_viewmodel.dart new file mode 100644 index 0000000..e69de29 diff --git a/lib/onboarding/views/onboarding_page.dart b/lib/onboarding/views/onboarding_page.dart new file mode 100644 index 0000000..8c08b51 --- /dev/null +++ b/lib/onboarding/views/onboarding_page.dart @@ -0,0 +1,109 @@ +import 'package:flutter/material.dart'; +import '../models/onboarding_model.dart'; +import 'widgets/glass_card.dart'; + +class OnboardingPage extends StatefulWidget { + const OnboardingPage({super.key}); + + @override + State createState() => _OnboardingPageState(); +} + +class _OnboardingPageState extends State + with SingleTickerProviderStateMixin { + int currentIndex = 0; + + final List onboardingData = [ + OnboardingModel( + image: 'assets/onboarding/bg1.jpg', + title: 'What we do', + description: + 'Easily manage and promote your attractions while connecting with travelers and handling bookings in one place.', + ), + OnboardingModel( + image: 'assets/onboarding/bg2.png', + title: 'Manage Booking Seamlessly', + description: + 'Track and manage all your bookings in real-time from one easy-to-use dashboard.', + ), + ]; + + late AnimationController _controller; + late Animation _fadeAnimation; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 800), + ); + _fadeAnimation = Tween(begin: 1.0, end: 0.0).animate(_controller); + } + + void _nextPage() async { + await _controller.forward(); + setState(() { + currentIndex = (currentIndex + 1) % onboardingData.length; + }); + _controller.reverse(); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final current = onboardingData[currentIndex]; + + return Scaffold( + body: Stack( + fit: StackFit.expand, + children: [ + FadeTransition( + opacity: _fadeAnimation.drive(Tween(begin: 0.1, end: 1.0)), + child: Image.asset(current.image, fit: BoxFit.cover), + ), + current.title == "Manage Booking Seamlessly" + ? Container() + : SafeArea( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Align( + alignment: Alignment.topRight, + child: OutlinedButton( + onPressed: () {}, + style: OutlinedButton.styleFrom( + side: const BorderSide(color: Colors.white), + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric( + horizontal: 20, + vertical: 8, + ), + ), + child: const Text("Skip"), + ), + ), + ), + ), + Align( + alignment: Alignment.bottomCenter, + child: Padding( + padding: const EdgeInsets.all(16.0), + child: GlassCard( + title: current.title, + description: current.description, + currentIndex: currentIndex, + total: onboardingData.length, + onContinue: _nextPage, + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/onboarding/views/widgets/glass_card.dart b/lib/onboarding/views/widgets/glass_card.dart new file mode 100644 index 0000000..fb00df1 --- /dev/null +++ b/lib/onboarding/views/widgets/glass_card.dart @@ -0,0 +1,94 @@ +import 'dart:ui'; +import 'package:flutter/material.dart'; + +class GlassCard extends StatelessWidget { + final String title; + final String description; + final int currentIndex; + final int total; + final VoidCallback onContinue; + + const GlassCard({ + super.key, + required this.title, + required this.description, + required this.currentIndex, + required this.total, + required this.onContinue, + }); + + @override + Widget build(BuildContext context) { + return ClipRRect( + borderRadius: BorderRadius.circular(16), + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 15, sigmaY: 15), + child: Container( + width: double.infinity, + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: Colors.black.withOpacity(0.4), + borderRadius: BorderRadius.circular(16), + border: Border.all(color: Colors.white.withOpacity(0.2)), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + title, + textAlign: TextAlign.center, + style: const TextStyle( + color: Colors.white, + fontSize: 20, + fontWeight: FontWeight.bold), + ), + const SizedBox(height: 8), + Text( + description, + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.white.withOpacity(0.9), + fontSize: 14, + ), + ), + const SizedBox(height: 16), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: List.generate( + total, + (index) => AnimatedContainer( + duration: const Duration(milliseconds: 300), + margin: const EdgeInsets.symmetric(horizontal: 4), + height: 6, + width: currentIndex == index ? 24 : 12, + decoration: BoxDecoration( + color: currentIndex == index + ? const Color(0xFFE25E5E) + : Colors.white24, + borderRadius: BorderRadius.circular(3), + ), + ), + ), + ), + const SizedBox(height: 20), + ElevatedButton( + onPressed: onContinue, + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFFE25E5E), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + minimumSize: const Size(double.infinity, 48), + ), + child: const Text( + 'Continue', + style: TextStyle(fontSize: 16, color: Colors.white), + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/profile/blocs/profile_bloc.dart b/lib/profile/blocs/profile_bloc.dart new file mode 100644 index 0000000..bddfc13 --- /dev/null +++ b/lib/profile/blocs/profile_bloc.dart @@ -0,0 +1,32 @@ +// profile_cubit.dart +import 'package:flutter_bloc/flutter_bloc.dart'; + +import '../viewmodel/profile_viewmodel.dart'; + + +class ProfileCubit extends Cubit { + ProfileCubit() : super(const ProfileState()) { + loadProfile(); + } + + void loadProfile() async { + emit(state.copyWith(isLoading: true)); + + // Simulate API call delay + await Future.delayed(Duration(seconds: 1)); + + emit(state.copyWith( + name: 'Lila Hart', + phone: '(+971) 050 4245 564', + role: 'Ticket Counter Staff', + email: 'support@citycards.com', + lastLogin: '21, November 2024', + isLoading: false, + )); + } + + void logout() { + // Handle logout logic here + print("User logged out"); + } +} diff --git a/lib/profile/viewmodel/profile_viewmodel.dart b/lib/profile/viewmodel/profile_viewmodel.dart new file mode 100644 index 0000000..16a6da6 --- /dev/null +++ b/lib/profile/viewmodel/profile_viewmodel.dart @@ -0,0 +1,39 @@ +// profile_state.dart +class ProfileState { + final String name; + final String phone; + final String role; + final String email; + final String lastLogin; + final bool isLoading; + + const ProfileState({ + this.name = '', + this.phone = '', + this.role = '', + this.email = '', + this.lastLogin = '', + this.isLoading = false, + }); + + ProfileState copyWith({ + String? name, + String? phone, + String? role, + String? email, + String? lastLogin, + bool? isLoading, + }) { + return ProfileState( + name: name ?? this.name, + phone: phone ?? this.phone, + role: role ?? this.role, + email: email ?? this.email, + lastLogin: lastLogin ?? this.lastLogin, + isLoading: isLoading ?? this.isLoading, + ); + } + + @override + List get props => [name, phone, role, email, lastLogin, isLoading]; +} diff --git a/lib/profile/views/profile_page.dart b/lib/profile/views/profile_page.dart new file mode 100644 index 0000000..bb513c8 --- /dev/null +++ b/lib/profile/views/profile_page.dart @@ -0,0 +1,229 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import '../blocs/profile_bloc.dart'; +import '../viewmodel/profile_viewmodel.dart'; + +class ProfileScreen extends StatelessWidget { + const ProfileScreen({super.key}); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (_) => ProfileCubit(), + child: Scaffold( + backgroundColor: Colors.white, + body: SafeArea( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 20.0), + child: BlocBuilder( + builder: (context, state) { + if (state.isLoading) { + return Center(child: CircularProgressIndicator()); + } + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildHeaderSection(), + SizedBox(height: 32), + _buildCustomerDetailsSection(state), + SizedBox(height: 24), + _buildAdditionalInfoSection(state, context), + SizedBox(height: 24), + Spacer(), + _buildLastLoginSection(state, context), + const SizedBox(height: 20), + ], + ); + }, + ), + ), + ), + ), + ); + } + + Widget _buildHeaderSection() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 2.0, vertical: 14.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + GestureDetector( + child: Container( + width: 44, + height: 44, + decoration: const BoxDecoration( + color: Color(0xFFF95F62), + shape: BoxShape.circle, + ), + child: const Icon(Icons.arrow_back, color: Colors.white), + ), + ), + Text( + 'Profile', + style: const TextStyle( + fontWeight: FontWeight.w700, + fontSize: 28, + ), + ), + SizedBox( + width: 40, + ) + ], + ), + ), + SizedBox(height: 8), + Text( + 'Manage your account, update preferences, and customize app settings for a personalized experience.', + style: TextStyle( + fontSize: 14, + color: Colors.grey[600], + height: 1.4, + ), + ), + ], + ); + } + + Widget _buildCustomerDetailsSection(ProfileState state) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Customer Details', + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.w600, + color: Colors.black, + ), + ), + SizedBox(height: 16), + Container( + decoration: BoxDecoration( + border: Border.symmetric(horizontal: BorderSide(color: Colors.grey[300]!)), + ), + child: Column( + children: [ + _buildDetailRow('Name', state.name), + _buildDivider(), + _buildDetailRow('Phone', state.phone), + _buildDivider(), + _buildDetailRow('Role', state.role), + ], + ), + ), + ], + ); + } + + Widget _buildAdditionalInfoSection(ProfileState state, BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Additional Info', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, + color: Colors.black, + ), + ), + SizedBox(height: 16), + Container( + decoration: BoxDecoration( + border: Border.symmetric(horizontal: BorderSide(color: Colors.grey[300]!)), + ), + child: Column( + children: [ + _buildActionRow('Change Email', state.email), + _buildDivider(), + _buildActionRow('Change Password', '● ● ● ● ● ● ●'), + ], + ), + ), + ], + ); + } + + Widget _buildLastLoginSection(ProfileState state, BuildContext context) { + return Column( + children: [ + SizedBox( + width: double.infinity, + child: Row( + children: [ + Text( + 'Last Login on ', + style: TextStyle(fontSize: 14, color: Colors.grey[600]), + ), + Text( + state.lastLogin, + style: TextStyle(fontSize: 14, color: Colors.black), + ), + ], + ), + ), + const SizedBox(height: 14), + SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: () => context.read().logout(), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.white, + padding: EdgeInsets.symmetric(horizontal: 16, vertical: 12), + side: BorderSide(color: Color(0xffDC2626)), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + ), + child: Row( + children: [ + Icon(Icons.logout, color: Color(0xffDC2626)), + SizedBox(width: 10), + Text( + 'Log out', + style: TextStyle(fontSize: 16, color: Color(0xffDC2626), fontWeight: FontWeight.w600), + ), + ], + ), + ), + ), + ], + ); + } + + Widget _buildDetailRow(String label, String value) { + return Padding( + padding: EdgeInsets.symmetric(horizontal: 16, vertical: 12), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded(flex: 2, child: Text(label, style: TextStyle(fontWeight: FontWeight.w500, color: Colors.grey[700]))), + Expanded(flex: 3, child: Text(value, style: TextStyle(color: Colors.black))), + ], + ), + ); + } + + Widget _buildActionRow(String label, String value) { + return Padding( + padding: EdgeInsets.symmetric(horizontal: 16, vertical: 12), + child: Row( + children: [ + Expanded(flex: 2, child: Text(label, style: TextStyle(fontWeight: FontWeight.w500, color: Colors.grey[700]))), + Expanded( + flex: 3, + child: Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ + Text(value, style: TextStyle(color: Colors.grey[600])), + ]), + ), + ], + ), + ); + } + + Widget _buildDivider() { + return Container(height: 1, color: Colors.grey[300], margin: EdgeInsets.symmetric(horizontal: 2)); + } +} diff --git a/lib/scan_history/blocs/scan_history_bloc.dart b/lib/scan_history/blocs/scan_history_bloc.dart new file mode 100644 index 0000000..3383be7 --- /dev/null +++ b/lib/scan_history/blocs/scan_history_bloc.dart @@ -0,0 +1,89 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; +import '../viewmodels/scan_history_viewmodel.dart'; +import '../models/scan_history_model.dart'; + +// EVENTS +abstract class ScanHistoryEvent {} + +class LoadScanHistory extends ScanHistoryEvent {} + +class UpdateDate extends ScanHistoryEvent { + final DateTime date; + UpdateDate(this.date); +} + +class UpdateStatus extends ScanHistoryEvent { + final String status; + UpdateStatus(this.status); +} + +// STATES +class ScanHistoryState { + final DateTime selectedDate; + final String selectedStatus; + final bool isLoading; + final List history; + final String? error; + + ScanHistoryState({ + required this.selectedDate, + required this.selectedStatus, + required this.isLoading, + required this.history, + this.error, + }); + + factory ScanHistoryState.initial() => ScanHistoryState( + selectedDate: DateTime.now(), + selectedStatus: 'All Status', + isLoading: false, + history: [], + ); + + ScanHistoryState copyWith({ + DateTime? selectedDate, + String? selectedStatus, + bool? isLoading, + List? history, + String? error, + }) { + return ScanHistoryState( + selectedDate: selectedDate ?? this.selectedDate, + selectedStatus: selectedStatus ?? this.selectedStatus, + isLoading: isLoading ?? this.isLoading, + history: history ?? this.history, + error: error, + ); + } +} + +// BLOC +class ScanHistoryBloc extends Bloc { + final ScanHistoryViewModel viewModel; + + ScanHistoryBloc({required this.viewModel}) : super(ScanHistoryState.initial()) { + on(_onLoadHistory); + on(_onUpdateDate); + on(_onUpdateStatus); + } + + Future _onLoadHistory(LoadScanHistory event, Emitter emit) async { + emit(state.copyWith(isLoading: true)); + try { + final data = await viewModel.getScanHistory(state.selectedDate, state.selectedStatus); + emit(state.copyWith(isLoading: false, history: data, error: null)); + } catch (_) { + emit(state.copyWith(isLoading: false, error: "Error loading data")); + } + } + + void _onUpdateDate(UpdateDate event, Emitter emit) { + emit(state.copyWith(selectedDate: event.date)); + add(LoadScanHistory()); + } + + void _onUpdateStatus(UpdateStatus event, Emitter emit) { + emit(state.copyWith(selectedStatus: event.status)); + add(LoadScanHistory()); + } +} diff --git a/lib/scan_history/blocs/scan_history_detail_bloc.dart b/lib/scan_history/blocs/scan_history_detail_bloc.dart new file mode 100644 index 0000000..14b50fd --- /dev/null +++ b/lib/scan_history/blocs/scan_history_detail_bloc.dart @@ -0,0 +1,71 @@ +import 'package:bloc/bloc.dart'; + + +// Events +abstract class ScanHistoryDetailEvent { + List get props => []; +} + +class LoadScanHistoryDetail extends ScanHistoryDetailEvent { + final String passId; + LoadScanHistoryDetail(this.passId); + + @override + List get props => [passId]; +} + +// States +abstract class ScanHistoryDetailState { + @override + List get props => []; +} + +class ScanHistoryDetailInitial extends ScanHistoryDetailState {} + +class ScanHistoryDetailLoading extends ScanHistoryDetailState {} + +class ScanHistoryDetailLoaded extends ScanHistoryDetailState { + final Map data; + ScanHistoryDetailLoaded(this.data); + + @override + List get props => [data]; +} + +class ScanHistoryDetailError extends ScanHistoryDetailState { + final String message; + ScanHistoryDetailError(this.message); + + @override + List get props => [message]; +} + +// Bloc +class ScanHistoryDetailBloc extends Bloc { + ScanHistoryDetailBloc() : super(ScanHistoryDetailInitial()) { + on(_onLoadPassDetail); + } + + Future _onLoadPassDetail(LoadScanHistoryDetail event, Emitter emit) async { + try { + emit(ScanHistoryDetailLoading()); + + // Placeholder: Replace with repository call later + await Future.delayed(const Duration(milliseconds: 200)); + + final dummy = { + 'cardType': 'Selective', + 'validity': 'Valid', + 'customerName': 'Lila Heart', + 'phone': '(+971) 050 421 4456', + 'email': 'ninarokz21@gmail.com', + 'city': 'The Enchanted Garden', + 'attractionName': 'The Enchanted Garden', + }; + + emit(ScanHistoryDetailLoaded(dummy)); + } catch (e) { + emit(ScanHistoryDetailError(e.toString())); + } + } +} diff --git a/lib/scan_history/models/scan_history_model.dart b/lib/scan_history/models/scan_history_model.dart new file mode 100644 index 0000000..e8315a0 --- /dev/null +++ b/lib/scan_history/models/scan_history_model.dart @@ -0,0 +1,13 @@ +class ScanHistoryModel { + final String passId; + final String? reason; + final String time; + final String status; // "Success" or "Failed" + + ScanHistoryModel({ + required this.passId, + this.reason, + required this.time, + required this.status, + }); +} diff --git a/lib/scan_history/repositories/scan_history_repository.dart b/lib/scan_history/repositories/scan_history_repository.dart new file mode 100644 index 0000000..2e4c1d5 --- /dev/null +++ b/lib/scan_history/repositories/scan_history_repository.dart @@ -0,0 +1,18 @@ +import '../models/scan_history_model.dart'; + +class ScanHistoryRepository { + Future> fetchScanHistory(DateTime date, String statusFilter) async { + await Future.delayed(const Duration(milliseconds: 500)); + + // Mock data + List data = [ + ScanHistoryModel(passId: '#P214125125', reason: 'Already Used', time: '05/11/24 on 11:00PM', status: 'Failed'), + ScanHistoryModel(passId: '#P214125125', time: '05/11/24 on 11:00PM', status: 'Success'), + ScanHistoryModel(passId: '#P214125125', reason: 'Invalid Code', time: '05/11/24 on 11:00PM', status: 'Failed'), + ScanHistoryModel(passId: '#P214125125', time: '05/11/24 on 11:00PM', status: 'Success'), + ]; + + if (statusFilter == 'All Status') return data; + return data.where((item) => item.status == statusFilter).toList(); + } +} diff --git a/lib/scan_history/viewmodels/scan_history_detail_viewmodel.dart b/lib/scan_history/viewmodels/scan_history_detail_viewmodel.dart new file mode 100644 index 0000000..0961b7d --- /dev/null +++ b/lib/scan_history/viewmodels/scan_history_detail_viewmodel.dart @@ -0,0 +1,34 @@ +class PassDetailViewModel { + final String displayPassId; + final String cardType; + final String validity; + final String customerName; + final String phone; + final String email; + final String city; + final String attractionName; + + PassDetailViewModel({ + required this.displayPassId, + required this.cardType, + required this.validity, + required this.customerName, + required this.phone, + required this.email, + required this.city, + required this.attractionName, + }); + + factory PassDetailViewModel.fromMap(Map data) { + return PassDetailViewModel( + displayPassId: data['passId'] ?? 'P214125125', + cardType: data['cardType'] ?? 'Selective', + validity: data['validity'] ?? 'Valid', + customerName: data['customerName'] ?? 'Lila Heart', + phone: data['phone'] ?? '(+971) 050 421 4456', + email: data['email'] ?? 'ninarokz21@gmail.com', + city: data['city'] ?? 'The Enchanted Garden', + attractionName: data['attractionName'] ?? 'The Enchanted Garden', + ); + } +} diff --git a/lib/scan_history/viewmodels/scan_history_viewmodel.dart b/lib/scan_history/viewmodels/scan_history_viewmodel.dart new file mode 100644 index 0000000..8ec7b4c --- /dev/null +++ b/lib/scan_history/viewmodels/scan_history_viewmodel.dart @@ -0,0 +1,12 @@ +import '../repositories/scan_history_repository.dart'; +import '../models/scan_history_model.dart'; + +class ScanHistoryViewModel { + final ScanHistoryRepository repository; + + ScanHistoryViewModel({required this.repository}); + + Future> getScanHistory(DateTime date, String statusFilter) async { + return await repository.fetchScanHistory(date, statusFilter); + } +} diff --git a/lib/scan_history/views/scan_history_detail_page.dart b/lib/scan_history/views/scan_history_detail_page.dart new file mode 100644 index 0000000..0ba2271 --- /dev/null +++ b/lib/scan_history/views/scan_history_detail_page.dart @@ -0,0 +1,188 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import '../blocs/scan_history_detail_bloc.dart'; +import '../viewmodels/scan_history_detail_viewmodel.dart'; + + +class ScanHistoryDetailPage extends StatelessWidget { + final String passId; + const ScanHistoryDetailPage({super.key, required this.passId}); + + TextStyle _headerStyle() => const TextStyle( + fontFamily: 'Poppins', + fontWeight: FontWeight.w600, + fontSize: 24, + height: 1.0, + ); + + TextStyle _labelStyle() => const TextStyle( + fontFamily: 'Poppins', + fontWeight: FontWeight.w500, + fontSize: 14, + color: Color(0xFF9E9E9E), + ); + + TextStyle _valueStyle() => const TextStyle( + fontFamily: 'Poppins', + fontWeight: FontWeight.w400, + fontSize: 12, + color: Colors.black, + ); + + Widget _twoColumnRow(String leftLabel, String leftValue, String rightLabel, String rightValue) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(leftLabel, style: _labelStyle()), + const SizedBox(height: 6), + Text(leftValue, style: _valueStyle()), + ], + ), + ), + SizedBox(width: 20,), + Expanded( + flex: 2, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(rightLabel, style: _labelStyle()), + const SizedBox(height: 6), + Text(rightValue, style: _valueStyle()), + ], + ), + ), + ], + ), + ); + } + + Widget _divider() => const Divider(color: Color(0xFFDDDDDD), height: 20, thickness: 1); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (_) => ScanHistoryDetailBloc()..add(LoadScanHistoryDetail(passId)), + child: Scaffold( + backgroundColor: Colors.white, + body: SafeArea( + child: BlocBuilder( + builder: (context, state) { + if (state is ScanHistoryDetailLoading) { + return const Center(child: CircularProgressIndicator()); + } + if (state is ScanHistoryDetailLoaded) { + final vm = PassDetailViewModel.fromMap(state.data); + return Column( + children: [ + // ======= HEADER ======= + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20.0, vertical: 14.0), + child: Row( + children: [ + GestureDetector( + // onTap: () => Navigator.pop(context), + child: Container( + width: 44, + height: 44, + decoration: const BoxDecoration( + color: Color(0xFFF06969), + shape: BoxShape.circle, + ), + child: const Icon(Icons.arrow_back, color: Colors.white), + ), + ), + const SizedBox(width: 12), + Expanded( + child: Center( + child: Text( + '#${vm.displayPassId}', + style: const TextStyle( + fontFamily: 'Poppins', + fontWeight: FontWeight.w700, + fontSize: 28, + ), + ), + ), + ), + const SizedBox(width: 12), + Container( + width: 24, + height: 24, + decoration: BoxDecoration( + border: Border.all(color: const Color(0xFFF06969), width: 2), + shape: BoxShape.circle, + ), + child: const Icon(Icons.person_outline, size: 16, color: Color(0xFFF06969)), + ), + ], + ), + ), + + // ======= BODY ======= + Expanded( + child: SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 20.0, vertical: 8.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Pass Summary + Text('Pass Summary', style: _headerStyle()), + _doubleDivider(), + _twoColumnRow('Card Type', vm.cardType, 'Validity', vm.validity), + + const SizedBox(height: 16), + Text('Customer Details', style: _headerStyle()), + _doubleDivider(), + _twoColumnRow('Customer Name', vm.customerName, 'Phone', vm.phone), + _divider(), + Text('Email', style: _labelStyle()), + const SizedBox(height: 6), + Text(vm.email, style: _valueStyle()), + + const SizedBox(height: 16), + Text('Attraction Details', style: _headerStyle()), + _doubleDivider(), + _twoColumnRow('City', vm.city, 'Attraction Name', vm.attractionName), + + const SizedBox(height: 60), + ], + ), + ), + ), + ], + ); + } + + if (state is ScanHistoryDetailError) { + return Center(child: Text(state.message)); + } + + return const SizedBox.shrink(); + }, + ), + ), + ), + ); + } + Widget _doubleDivider() { + return Row( + children: [ + Expanded(flex:1 ,child: _divider()), + const SizedBox(width: 20), + Expanded(flex:2 ,child: _divider()), + ], + ); + } + + + +} + + + diff --git a/lib/scan_history/views/scan_history_page.dart b/lib/scan_history/views/scan_history_page.dart new file mode 100644 index 0000000..4ca86dc --- /dev/null +++ b/lib/scan_history/views/scan_history_page.dart @@ -0,0 +1,270 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:google_fonts/google_fonts.dart'; +import '../blocs/scan_history_bloc.dart'; +import '../repositories/scan_history_repository.dart'; +import '../viewmodels/scan_history_viewmodel.dart'; +import '../models/scan_history_model.dart'; + +class ScanHistoryPage extends StatelessWidget { + const ScanHistoryPage({super.key}); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (_) => ScanHistoryBloc( + viewModel: ScanHistoryViewModel(repository: ScanHistoryRepository()), + )..add(LoadScanHistory()), + child: const _ScanHistoryView(), + ); + } +} + +class _ScanHistoryView extends StatelessWidget { + const _ScanHistoryView(); + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.white, + appBar: AppBar( + backgroundColor: Colors.white, + elevation: 0, + centerTitle: true, + title: Text( + "Scan History", + style: GoogleFonts.poppins( + fontWeight: FontWeight.w700, + color: Colors.black, + ), + ), + leading: const Padding( + padding: EdgeInsets.only(left: 12), + child: CircleAvatar( + maxRadius: 44, + backgroundColor: Color(0xffF95F62), + child: Icon(Icons.arrow_back, color: Colors.white)), + ), + actions: const [ + Padding( + padding: EdgeInsets.only(right: 12), + child: Icon(Icons.account_circle, color: Color(0xffF95F62)), + ), + ], + ), + body: Padding( + padding: const EdgeInsets.all(16), + child: BlocBuilder( + builder: (context, state) { + final bloc = context.read(); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "View a detailed log of all scanned QR codes, including timestamps, results, and customer details for easy tracking and verification.", + style: GoogleFonts.poppins( + fontSize: 14, + color: Colors.black,fontWeight: FontWeight.w400 + ), + ), + const SizedBox(height: 16), + + // Date + Status Row + Row( + children: [ + Expanded( + child: GestureDetector( + onTap: () async { + final picked = await showDatePicker( + context: context, + initialDate: state.selectedDate, + firstDate: DateTime(2020), + lastDate: DateTime(2030), + builder: (context, child) { + return Theme( + data: Theme.of(context).copyWith( + colorScheme: const ColorScheme.light( + primary: Color(0xffF95F62), + ), + ), + child: child!, + ); + }, + ); + if (picked != null) { + bloc.add(UpdateDate(picked)); + } + }, + child: Row( + children: [ + Text("Date", + style: GoogleFonts.poppins( + fontSize: 13, color: Colors.black,fontWeight: FontWeight.w400)), + const SizedBox(width: 12), + Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + decoration: BoxDecoration( + color: const Color(0xffF8F8F8), + borderRadius: BorderRadius.circular(50), + ), + child: Row( + children: [ + const Icon(Icons.calendar_month, + color: Color(0xffF95F62), size: 18), + const SizedBox(width: 8), + Text( + "${state.selectedDate.day.toString().padLeft(2, '0')}/${state.selectedDate.month.toString().padLeft(2, '0')}/${state.selectedDate.year.toString().substring(2)}", + style: GoogleFonts.poppins(fontSize: 13), + ), + ], + ), + ), + ], + ), + ), + ), + const SizedBox(width: 16), + + // Status Dropdown + Expanded( + child: Row( + children: [ + Text("Status", + style: GoogleFonts.poppins( + fontSize: 14, color: Colors.black,fontWeight: FontWeight.w400)), + const SizedBox(width: 12), + Expanded( + child: Container( + decoration: BoxDecoration( + color: const Color(0xffF8F8F8), + borderRadius: BorderRadius.circular(50), + ), + padding: const EdgeInsets.symmetric(horizontal: 12), + child: DropdownButtonHideUnderline( + child: DropdownButton( + isExpanded: true, + dropdownColor: Colors.white, + value: state.selectedStatus, + icon: const Icon(Icons.keyboard_arrow_down_rounded,size: 26, + color: Color(0xffF95F62)), + items: const [ + DropdownMenuItem( + value: 'All Status', + child: Text("All Status")), + DropdownMenuItem( + value: 'Failed', child: Text("Failed")), + DropdownMenuItem( + value: 'Success', + child: Text("Success")), + ], + onChanged: (val) { + if (val != null) bloc.add(UpdateStatus(val)); + }, + style: GoogleFonts.poppins( + fontSize: 14, color: Colors.black,fontWeight: FontWeight.w400), + ), + ), + ), + ), + ], + ), + ), + ], + ), + + const SizedBox(height: 20), + + // Data Section + Expanded(child: _buildStateUI(state)), + ], + ); + }, + ), + ), + ); + } + + Widget _buildStateUI(ScanHistoryState state) { + if (state.isLoading) { + return const Center( + child: CircularProgressIndicator(color: Color(0xffF95F62)), + ); + } else if (state.error != null) { + return Center(child: Text(state.error!)); + } else if (state.history.isEmpty) { + return Center( + child: Text("No records found", + style: GoogleFonts.poppins(color: Colors.black54)), + ); + } + + return ListView.builder( + itemCount: state.history.length, + itemBuilder: (context, index) { + final item = state.history[index]; + return _buildCard(item); + }, + ); + } + + Widget _buildCard(ScanHistoryModel item) { + final isSuccess = item.status == "Success"; + return Container( + margin: const EdgeInsets.only(bottom: 12), + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: isSuccess ? const Color(0xffE9F9EF) : const Color(0xffFCEAEA), + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: isSuccess ? const Color(0xffB5E5C1) : const Color(0xffF5B1B1)), + ), + child: Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text("Pass ID: ${item.passId}", + style: GoogleFonts.poppins( + fontWeight: FontWeight.w600, fontSize: 13)), + if (item.reason != null) ...[ + const SizedBox(height: 4), + Text("Reason: ${item.reason!}", + style: GoogleFonts.poppins( + fontSize: 12, color: Colors.black87)), + ], + const SizedBox(height: 4), + Text("Time: ${item.time}", + style: GoogleFonts.poppins( + fontSize: 12, color: Colors.black87)), + const SizedBox(height: 8), + Row( + children: [ + Icon( + isSuccess ? Icons.check_circle : Icons.cancel, + color: isSuccess + ? const Color(0xff2DCC70) + : const Color(0xffF95F62), + size: 20, + ), + const SizedBox(width: 6), + Text( + item.status, + style: GoogleFonts.poppins( + color: Colors.black, + fontWeight: FontWeight.w600, + fontSize: 24, + ), + ), + ], + ), + ], + ), + ), + const Icon(Icons.qr_code_2_sharp, size: 78, color: Colors.black45), + ], + ), + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index c1b51d3..aa2fd7c 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -9,6 +9,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.13.0" + bloc: + dependency: transitive + description: + name: bloc + sha256: e18b8e7825e9921d67a6d256dba0b6015ece8a577eb0a411845c46a352994d78 + url: "https://pub.dev" + source: hosted + version: "9.0.1" boolean_selector: dependency: transitive description: @@ -41,6 +49,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.19.1" + crypto: + dependency: transitive + description: + name: crypto + sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" + url: "https://pub.dev" + source: hosted + version: "3.0.6" cupertino_icons: dependency: "direct main" description: @@ -57,11 +73,27 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.3" + ffi: + dependency: transitive + description: + name: ffi + sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418" + url: "https://pub.dev" + source: hosted + version: "2.1.4" flutter: dependency: "direct main" description: flutter source: sdk version: "0.0.0" + flutter_bloc: + dependency: "direct main" + description: + name: flutter_bloc + sha256: cf51747952201a455a1c840f8171d273be009b932c75093020f9af64f2123e38 + url: "https://pub.dev" + source: hosted + version: "9.1.1" flutter_lints: dependency: "direct dev" description: @@ -70,11 +102,43 @@ packages: url: "https://pub.dev" source: hosted version: "5.0.0" + flutter_otp_text_field: + dependency: "direct main" + description: + name: flutter_otp_text_field + sha256: e7e589dc51cde120d63da6db55f3cef618f5d013d12adba76137ca1a51ce1390 + url: "https://pub.dev" + source: hosted + version: "1.5.1+1" flutter_test: dependency: "direct dev" description: flutter source: sdk version: "0.0.0" + google_fonts: + dependency: "direct main" + description: + name: google_fonts + sha256: "517b20870220c48752eafa0ba1a797a092fb22df0d89535fd9991e86ee2cdd9c" + url: "https://pub.dev" + source: hosted + version: "6.3.2" + http: + dependency: transitive + description: + name: http + sha256: bb2ce4590bc2667c96f318d68cac1b5a7987ec819351d32b1c987239a815e007 + url: "https://pub.dev" + source: hosted + version: "1.5.0" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" + url: "https://pub.dev" + source: hosted + version: "4.1.2" leak_tracker: dependency: transitive description: @@ -131,6 +195,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.16.0" + nested: + dependency: transitive + description: + name: nested + sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" + url: "https://pub.dev" + source: hosted + version: "1.0.0" path: dependency: transitive description: @@ -139,6 +211,78 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.1" + path_provider: + dependency: transitive + description: + name: path_provider + sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" + url: "https://pub.dev" + source: hosted + version: "2.1.5" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: "3b4c1fc3aa55ddc9cd4aa6759984330d5c8e66aa7702a6223c61540dc6380c37" + url: "https://pub.dev" + source: hosted + version: "2.2.19" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "16eef174aacb07e09c351502740fa6254c165757638eba1e9116b0a781201bbd" + url: "https://pub.dev" + source: hosted + version: "2.4.2" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.dev" + source: hosted + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 + url: "https://pub.dev" + source: hosted + version: "2.3.0" + platform: + dependency: transitive + description: + name: platform + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" + url: "https://pub.dev" + source: hosted + version: "3.1.6" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" + provider: + dependency: transitive + description: + name: provider + sha256: "4e82183fa20e5ca25703ead7e05de9e4cceed1fbd1eadc1ac3cb6f565a09f272" + url: "https://pub.dev" + source: hosted + version: "6.1.5+1" sky_engine: dependency: transitive description: flutter @@ -192,6 +336,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.6" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.dev" + source: hosted + version: "1.4.0" vector_math: dependency: transitive description: @@ -208,6 +360,22 @@ packages: url: "https://pub.dev" source: hosted version: "15.0.2" + web: + dependency: transitive + description: + name: web + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" + url: "https://pub.dev" + source: hosted + version: "1.1.0" sdks: dart: ">=3.9.2 <4.0.0" - flutter: ">=3.18.0-18.0.pre.54" + flutter: ">=3.29.0" diff --git a/pubspec.yaml b/pubspec.yaml index 2c2a9eb..079c0b9 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -34,6 +34,9 @@ dependencies: # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.8 + google_fonts: ^6.3.2 + flutter_bloc: ^9.1.1 + flutter_otp_text_field: ^1.5.1+1 dev_dependencies: flutter_test: @@ -58,9 +61,10 @@ flutter: uses-material-design: true # To add assets to your application, add an assets section, like this: - # assets: - # - images/a_dot_burr.jpeg - # - images/a_dot_ham.jpeg + assets: + - assets/onboarding/ + - assets/login/ + # An image asset can refer to one or more resolution-specific "variants", see # https://flutter.dev/to/resolution-aware-images