second commit
This commit is contained in:
@@ -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'))),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
82
lib/login/blocs/forgot_password_bloc.dart
Normal file
82
lib/login/blocs/forgot_password_bloc.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
0
lib/login/blocs/login_bloc.dart
Normal file
0
lib/login/blocs/login_bloc.dart
Normal file
103
lib/login/blocs/otp_bloc.dart
Normal file
103
lib/login/blocs/otp_bloc.dart
Normal 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!'));
|
||||
});
|
||||
}
|
||||
}
|
||||
93
lib/login/blocs/reset_password_bloc.dart
Normal file
93
lib/login/blocs/reset_password_bloc.dart
Normal 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; // 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<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 (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<ConfirmPasswordChanged>((event, emit) {
|
||||
emit(state.copyWith(confirmPassword: event.confirmPassword));
|
||||
});
|
||||
}
|
||||
}
|
||||
0
lib/login/models/login.dart
Normal file
0
lib/login/models/login.dart
Normal file
0
lib/login/repositories/login_repository.dart
Normal file
0
lib/login/repositories/login_repository.dart
Normal file
0
lib/login/repositories/otp_repository.dart
Normal file
0
lib/login/repositories/otp_repository.dart
Normal file
0
lib/login/viewmodels/forgot_password_viewmodel.dart
Normal file
0
lib/login/viewmodels/forgot_password_viewmodel.dart
Normal file
0
lib/login/viewmodels/login_viewmodel.dart
Normal file
0
lib/login/viewmodels/login_viewmodel.dart
Normal file
0
lib/login/viewmodels/otp_viewmodel.dart
Normal file
0
lib/login/viewmodels/otp_viewmodel.dart
Normal file
0
lib/login/viewmodels/reset_password_viewmodel.dart
Normal file
0
lib/login/viewmodels/reset_password_viewmodel.dart
Normal file
204
lib/login/views/forgot_password_page.dart
Normal file
204
lib/login/views/forgot_password_page.dart
Normal 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? 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<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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
238
lib/login/views/login_page.dart
Normal file
238
lib/login/views/login_page.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
193
lib/login/views/otp_verification_page.dart
Normal file
193
lib/login/views/otp_verification_page.dart
Normal 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(
|
||||
"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<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,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
252
lib/login/views/reset_password_page.dart
Normal file
252
lib/login/views/reset_password_page.dart
Normal 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 (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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
0
lib/onboarding/blocs/onboarding_bloc.dart
Normal file
0
lib/onboarding/blocs/onboarding_bloc.dart
Normal file
11
lib/onboarding/models/onboarding_model.dart
Normal file
11
lib/onboarding/models/onboarding_model.dart
Normal 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,
|
||||
});
|
||||
}
|
||||
0
lib/onboarding/viewmodels/onboarding_viewmodel.dart
Normal file
0
lib/onboarding/viewmodels/onboarding_viewmodel.dart
Normal file
109
lib/onboarding/views/onboarding_page.dart
Normal file
109
lib/onboarding/views/onboarding_page.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
94
lib/onboarding/views/widgets/glass_card.dart
Normal file
94
lib/onboarding/views/widgets/glass_card.dart
Normal 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),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
32
lib/profile/blocs/profile_bloc.dart
Normal file
32
lib/profile/blocs/profile_bloc.dart
Normal 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");
|
||||
}
|
||||
}
|
||||
39
lib/profile/viewmodel/profile_viewmodel.dart
Normal file
39
lib/profile/viewmodel/profile_viewmodel.dart
Normal 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];
|
||||
}
|
||||
229
lib/profile/views/profile_page.dart
Normal file
229
lib/profile/views/profile_page.dart
Normal 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));
|
||||
}
|
||||
}
|
||||
89
lib/scan_history/blocs/scan_history_bloc.dart
Normal file
89
lib/scan_history/blocs/scan_history_bloc.dart
Normal 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());
|
||||
}
|
||||
}
|
||||
71
lib/scan_history/blocs/scan_history_detail_bloc.dart
Normal file
71
lib/scan_history/blocs/scan_history_detail_bloc.dart
Normal 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()));
|
||||
}
|
||||
}
|
||||
}
|
||||
13
lib/scan_history/models/scan_history_model.dart
Normal file
13
lib/scan_history/models/scan_history_model.dart
Normal 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,
|
||||
});
|
||||
}
|
||||
18
lib/scan_history/repositories/scan_history_repository.dart
Normal file
18
lib/scan_history/repositories/scan_history_repository.dart
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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',
|
||||
);
|
||||
}
|
||||
}
|
||||
12
lib/scan_history/viewmodels/scan_history_viewmodel.dart
Normal file
12
lib/scan_history/viewmodels/scan_history_viewmodel.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
188
lib/scan_history/views/scan_history_detail_page.dart
Normal file
188
lib/scan_history/views/scan_history_detail_page.dart
Normal 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()),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
270
lib/scan_history/views/scan_history_page.dart
Normal file
270
lib/scan_history/views/scan_history_page.dart
Normal 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),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user