second commit

This commit is contained in:
2025-10-15 17:16:59 +05:30
parent 73be5c6615
commit ee53254fe6
45 changed files with 2636 additions and 11 deletions

BIN
assets/login/app_icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

BIN
assets/login/bg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 MiB

BIN
assets/onboarding/bg1.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 543 KiB

BIN
assets/onboarding/bg2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 MiB

View File

@@ -1 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
#include "Generated.xcconfig"

View File

@@ -1 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
#include "Generated.xcconfig"

43
ios/Podfile Normal file
View File

@@ -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

View File

@@ -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<dynamic> 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'))),
);
}
}
}

View File

@@ -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<ForgotPasswordEvent, ForgotPasswordState> {
ForgotPasswordBloc() : super(ForgotPasswordState.initial()) {
// when email changes
on<EmailChanged>((event, emit) {
final isValid = _isValidEmail(event.email);
emit(state.copyWith(email: event.email, isValidEmail: isValid));
});
// when button clicked
on<SendResetLink>((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);
}
}

View File

View File

@@ -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<OtpEvent, OtpState> {
OtpBloc() : super(OtpState.initial()) {
// Handle typing input
on<OtpChanged>((event, emit) {
emit(state.copyWith(otp: event.otp, message: ''));
});
// Handle Verify
on<OtpVerify>((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<OtpResend>((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!'));
});
}
}

View File

@@ -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; // 04 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<ResetPasswordEvent, ResetPasswordState> {
ResetPasswordBloc() : super(ResetPasswordState.initial()) {
on<PasswordChanged>((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 (04)
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<ConfirmPasswordChanged>((event, emit) {
emit(state.copyWith(confirmPassword: event.confirmPassword));
});
}
}

View File

View File

View File

@@ -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<ForgotPasswordBloc>();
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<ForgotPasswordBloc, ForgotPasswordState>(
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? Dont worry — just enter your email and well 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<Color>(
(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,
),
),
),
),
],
);
},
),
),
),
],
),
);
}
}

View File

@@ -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<LoginPage> createState() => _LoginPageState();
}
class _LoginPageState extends State<LoginPage> {
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,
),
),
),
),
],
),
),
),
],
),
);
}
}

View File

@@ -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<OtpBloc, OtpState>(
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<OtpBloc>();
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(
"Weve 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<OtpBloc, OtpState>(
builder: (context, state) {
final otpBloc = context.read<OtpBloc>();
return SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: state.isOtpFilled && !state.isLoading
? () => otpBloc.add(OtpVerify())
: null,
style: ButtonStyle(
backgroundColor:
MaterialStateProperty.resolveWith<Color>((
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,
),
),
),
);
},
),
],
),
),
),
],
),
),
);
}
}

View File

@@ -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<ResetPasswordBloc>();
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<ResetPasswordBloc, ResetPasswordState>(
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 (AZ)",
state.hasUppercase),
_buildValidationRow(
"At least one number (09)", 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,
),
),
],
),
);
}
}

View File

@@ -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,
);
}
}

View File

@@ -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,
});
}

View File

@@ -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<OnboardingPage> createState() => _OnboardingPageState();
}
class _OnboardingPageState extends State<OnboardingPage>
with SingleTickerProviderStateMixin {
int currentIndex = 0;
final List<OnboardingModel> 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<double> _fadeAnimation;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 800),
);
_fadeAnimation = Tween<double>(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,
),
),
),
],
),
);
}
}

View File

@@ -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),
),
),
],
),
),
),
);
}
}

View File

@@ -0,0 +1,32 @@
// profile_cubit.dart
import 'package:flutter_bloc/flutter_bloc.dart';
import '../viewmodel/profile_viewmodel.dart';
class ProfileCubit extends Cubit<ProfileState> {
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");
}
}

View File

@@ -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<Object?> get props => [name, phone, role, email, lastLogin, isLoading];
}

View File

@@ -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<ProfileCubit, ProfileState>(
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<ProfileCubit>().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));
}
}

View File

@@ -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<ScanHistoryModel> 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<ScanHistoryModel>? 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<ScanHistoryEvent, ScanHistoryState> {
final ScanHistoryViewModel viewModel;
ScanHistoryBloc({required this.viewModel}) : super(ScanHistoryState.initial()) {
on<LoadScanHistory>(_onLoadHistory);
on<UpdateDate>(_onUpdateDate);
on<UpdateStatus>(_onUpdateStatus);
}
Future<void> _onLoadHistory(LoadScanHistory event, Emitter<ScanHistoryState> 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<ScanHistoryState> emit) {
emit(state.copyWith(selectedDate: event.date));
add(LoadScanHistory());
}
void _onUpdateStatus(UpdateStatus event, Emitter<ScanHistoryState> emit) {
emit(state.copyWith(selectedStatus: event.status));
add(LoadScanHistory());
}
}

View File

@@ -0,0 +1,71 @@
import 'package:bloc/bloc.dart';
// Events
abstract class ScanHistoryDetailEvent {
List<Object?> get props => [];
}
class LoadScanHistoryDetail extends ScanHistoryDetailEvent {
final String passId;
LoadScanHistoryDetail(this.passId);
@override
List<Object?> get props => [passId];
}
// States
abstract class ScanHistoryDetailState {
@override
List<Object?> get props => [];
}
class ScanHistoryDetailInitial extends ScanHistoryDetailState {}
class ScanHistoryDetailLoading extends ScanHistoryDetailState {}
class ScanHistoryDetailLoaded extends ScanHistoryDetailState {
final Map<String, dynamic> data;
ScanHistoryDetailLoaded(this.data);
@override
List<Object?> get props => [data];
}
class ScanHistoryDetailError extends ScanHistoryDetailState {
final String message;
ScanHistoryDetailError(this.message);
@override
List<Object?> get props => [message];
}
// Bloc
class ScanHistoryDetailBloc extends Bloc<ScanHistoryDetailEvent, ScanHistoryDetailState> {
ScanHistoryDetailBloc() : super(ScanHistoryDetailInitial()) {
on<LoadScanHistoryDetail>(_onLoadPassDetail);
}
Future<void> _onLoadPassDetail(LoadScanHistoryDetail event, Emitter<ScanHistoryDetailState> 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()));
}
}
}

View File

@@ -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,
});
}

View File

@@ -0,0 +1,18 @@
import '../models/scan_history_model.dart';
class ScanHistoryRepository {
Future<List<ScanHistoryModel>> fetchScanHistory(DateTime date, String statusFilter) async {
await Future.delayed(const Duration(milliseconds: 500));
// Mock data
List<ScanHistoryModel> 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();
}
}

View File

@@ -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<String, dynamic> 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',
);
}
}

View File

@@ -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<List<ScanHistoryModel>> getScanHistory(DateTime date, String statusFilter) async {
return await repository.fetchScanHistory(date, statusFilter);
}
}

View File

@@ -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<ScanHistoryDetailBloc, ScanHistoryDetailState>(
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()),
],
);
}
}

View File

@@ -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<ScanHistoryBloc, ScanHistoryState>(
builder: (context, state) {
final bloc = context.read<ScanHistoryBloc>();
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<String>(
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),
],
),
);
}
}

View File

@@ -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"

View File

@@ -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