added userdetails api get and put and more changes
This commit is contained in:
BIN
assets/images/guest_illustration.png
Normal file
BIN
assets/images/guest_illustration.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 102 KiB |
BIN
assets/images/not_login.png
Normal file
BIN
assets/images/not_login.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 104 KiB |
@@ -1,4 +1,3 @@
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class CustomText extends StatelessWidget {
|
||||
@@ -8,6 +7,7 @@ class CustomText extends StatelessWidget {
|
||||
final String text;
|
||||
final int? maxLines;
|
||||
final TextOverflow? overflow;
|
||||
final TextAlign? textAlign;
|
||||
|
||||
const CustomText({
|
||||
Key? key,
|
||||
@@ -17,6 +17,7 @@ class CustomText extends StatelessWidget {
|
||||
required this.text,
|
||||
this.maxLines,
|
||||
this.overflow,
|
||||
this.textAlign,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
@@ -37,7 +38,7 @@ class CustomText extends StatelessWidget {
|
||||
),
|
||||
maxLines: maxLines,
|
||||
overflow: overflow,
|
||||
textAlign: textAlign,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -7,7 +7,12 @@ class CustomTextField extends StatelessWidget {
|
||||
final String hint;
|
||||
final TextEditingController controller;
|
||||
final int? maxLines;
|
||||
final bool enabled; // ✅ NEW PARAMETER
|
||||
final bool enabled;
|
||||
final String? Function(String?)? validator; // ✅ NEW: Validator function
|
||||
final TextInputType? keyboardType; // ✅ NEW: Keyboard type
|
||||
final bool obscureText; // ✅ NEW: For password fields
|
||||
final Widget? suffixIcon; // ✅ NEW: For icons like visibility toggle
|
||||
final void Function(String)? onChanged; // ✅ NEW: OnChanged callback
|
||||
|
||||
const CustomTextField({
|
||||
super.key,
|
||||
@@ -15,7 +20,12 @@ class CustomTextField extends StatelessWidget {
|
||||
required this.hint,
|
||||
required this.controller,
|
||||
this.maxLines = 1,
|
||||
this.enabled = true, // ✅ default enabled
|
||||
this.enabled = true,
|
||||
this.validator,
|
||||
this.keyboardType,
|
||||
this.obscureText = false,
|
||||
this.suffixIcon,
|
||||
this.onChanged,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -32,10 +42,14 @@ class CustomTextField extends StatelessWidget {
|
||||
SizedBox(height: 6.h),
|
||||
SizedBox(
|
||||
height: maxLines == 1 ? 42.h : null,
|
||||
child: TextField(
|
||||
child: TextFormField( // ✅ Changed from TextField to TextFormField
|
||||
controller: controller,
|
||||
maxLines: maxLines,
|
||||
enabled: enabled, // ✅ applied here
|
||||
maxLines: obscureText ? 1 : maxLines, // ✅ Password fields always single line
|
||||
enabled: enabled,
|
||||
validator: validator, // ✅ Added validator
|
||||
keyboardType: keyboardType, // ✅ Added keyboard type
|
||||
obscureText: obscureText, // ✅ Added obscure text
|
||||
onChanged: onChanged, // ✅ Added onChanged
|
||||
decoration: InputDecoration(
|
||||
hintText: hint,
|
||||
hintStyle: TextStyle(
|
||||
@@ -45,8 +59,12 @@ class CustomTextField extends StatelessWidget {
|
||||
filled: true,
|
||||
fillColor: enabled
|
||||
? const Color(0xFFFFF5F5)
|
||||
: Colors.grey.shade200, // subtle disabled look
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 24.w),
|
||||
: Colors.grey.shade200,
|
||||
contentPadding: EdgeInsets.symmetric(
|
||||
horizontal: 24.w,
|
||||
vertical: maxLines != null && maxLines! > 1 ? 12.h : 0, // ✅ Better padding for multiline
|
||||
),
|
||||
suffixIcon: suffixIcon, // ✅ Added suffix icon
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8.r),
|
||||
borderSide: BorderSide(
|
||||
@@ -68,6 +86,24 @@ class CustomTextField extends StatelessWidget {
|
||||
width: .4.w,
|
||||
),
|
||||
),
|
||||
errorBorder: OutlineInputBorder( // ✅ NEW: Error state border
|
||||
borderRadius: BorderRadius.circular(8.r),
|
||||
borderSide: BorderSide(
|
||||
color: Colors.red,
|
||||
width: 1.w,
|
||||
),
|
||||
),
|
||||
focusedErrorBorder: OutlineInputBorder( // ✅ NEW: Focused error state
|
||||
borderRadius: BorderRadius.circular(8.r),
|
||||
borderSide: BorderSide(
|
||||
color: Colors.red,
|
||||
width: 1.5.w,
|
||||
),
|
||||
),
|
||||
errorStyle: TextStyle( // ✅ NEW: Error text style
|
||||
fontSize: 11.sp,
|
||||
color: Colors.red,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -75,4 +111,4 @@ class CustomTextField extends StatelessWidget {
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,7 @@ import 'package:citycards_customer/itinerary_creation/bloc/itinerary_steps_selec
|
||||
import 'package:citycards_customer/itinerary_creation/views/itinerary_creation_start_view.dart';
|
||||
import 'package:citycards_customer/itinerary_creation/views/itinerary_creation_view.dart';
|
||||
import 'package:citycards_customer/itinerary_creation/views/magic_itinerary_empty_view.dart';
|
||||
import 'package:citycards_customer/itinerary_creation/views/magic_itinerary_filled_view.dart';
|
||||
import 'package:citycards_customer/itinerary_creation/views/magic_itinerary_view.dart';
|
||||
import 'package:citycards_customer/offer_pass_detail/offer_pass_detail_view.dart';
|
||||
import 'package:citycards_customer/search_offers/bloc/search_offers_listing_bloc.dart';
|
||||
import 'package:citycards_customer/search_offers/view/search_offers_with_listing.dart';
|
||||
@@ -221,7 +221,7 @@ class AppRouter {
|
||||
case RouteConstants.magicItineraryFilledScreen:
|
||||
return MaterialPageRoute(
|
||||
builder: (_) {
|
||||
return MagicItineraryFilledView();
|
||||
return MagicItineraryView();
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ import '../intro_screens/views/intro_screen_view.dart';
|
||||
import '../itinerary_creation/bloc/itinerary_detail_bloc.dart';
|
||||
import '../itinerary_creation/bloc/itinerary_steps_selection_bloc.dart';
|
||||
import '../itinerary_creation/views/itinerary_creation_view.dart';
|
||||
import '../itinerary_creation/views/magic_itinerary_filled_view.dart';
|
||||
import '../itinerary_creation/views/magic_itinerary_view.dart';
|
||||
import '../my_pass/views/booking_page_view.dart';
|
||||
import '../my_pass/views/booking_successful_page_view.dart';
|
||||
import '../my_pass/views/qr_pass_page_view.dart';
|
||||
@@ -155,7 +155,7 @@ Widget buildOffstageNavigator(
|
||||
|
||||
case RouteConstants.magicItineraryFilledScreen:
|
||||
return MaterialPageRoute(builder: (_){
|
||||
return MagicItineraryFilledView();
|
||||
return MagicItineraryView();
|
||||
});
|
||||
|
||||
case RouteConstants.checkout:
|
||||
|
||||
@@ -36,6 +36,15 @@ class CreateAccountBloc extends Bloc<CreateAccountEvent, CreateAccountState> {
|
||||
refreshToken: userModel.refreshToken,
|
||||
refreshTokenMaxAge: userModel.refreshTokenMaxAge,
|
||||
);
|
||||
await LocalPreference.setUserDetails(
|
||||
userId: userModel.user.id,
|
||||
firstName: userModel.user.firstName,
|
||||
lastName: userModel.user.lastName,
|
||||
fullName: userModel.user.fullName,
|
||||
emailAddress: userModel.user.emailAddress,
|
||||
role: userModel.user.role,
|
||||
roleId: userModel.user.roleId,
|
||||
);
|
||||
emit(CreateAccountSuccess(
|
||||
message: response['message'] ?? 'Account created successfully',
|
||||
userData: response['data'] ?? {},
|
||||
|
||||
@@ -4,169 +4,351 @@ import 'package:citycards_customer/common_packages/custom_textfield.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:citycards_customer/common_packages/custom_text.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class EditProfilePage extends StatelessWidget {
|
||||
import '../../localPreference/local_preference.dart';
|
||||
import '../profile/bloc/profile/profile_bloc.dart';
|
||||
import '../profile/bloc/profile/profile_event.dart';
|
||||
import '../profile/bloc/profile/profile_state.dart';
|
||||
import '../profile/models/profile_model.dart';
|
||||
|
||||
class EditProfilePage extends StatefulWidget {
|
||||
const EditProfilePage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final TextEditingController firstNameController = TextEditingController();
|
||||
final TextEditingController lastNameController = TextEditingController();
|
||||
final TextEditingController emailController = TextEditingController();
|
||||
final TextEditingController phoneController = TextEditingController();
|
||||
final TextEditingController addressController = TextEditingController();
|
||||
State<EditProfilePage> createState() => _EditProfilePageState();
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
body: SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 10.h),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
// Header
|
||||
CommonAppBar(isWhiteLogo: false, isProfilePage: true,showDivider: true,),
|
||||
class _EditProfilePageState extends State<EditProfilePage> {
|
||||
// Controllers
|
||||
final TextEditingController firstNameController = TextEditingController();
|
||||
final TextEditingController lastNameController = TextEditingController();
|
||||
final TextEditingController phoneController = TextEditingController();
|
||||
final TextEditingController address1Controller = TextEditingController();
|
||||
final TextEditingController address2Controller = TextEditingController();
|
||||
|
||||
// Back + title
|
||||
backWidget(context,"Edit Profile", Colors.black),
|
||||
SizedBox(height: 33.h),
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
|
||||
// Profile Image
|
||||
CircleAvatar(
|
||||
radius: 38.r,
|
||||
backgroundImage: AssetImage("assets/images/profile_img.png"),
|
||||
),
|
||||
SizedBox(height: 18.h),
|
||||
Text(
|
||||
"Change Profile Picture",
|
||||
style: TextStyle(
|
||||
fontSize: 12.sp,
|
||||
color: Color(0xFFF95F62),
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 40.h),
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_fetchProfile();
|
||||
}
|
||||
|
||||
// Personal Information
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: CustomText(
|
||||
text: "Personal Information",
|
||||
size: 18.sp,
|
||||
weight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 12.h),
|
||||
Future<void> _fetchProfile() async {
|
||||
final userId = await LocalPreference.getUserId();
|
||||
if (userId != null) {
|
||||
context.read<ProfileBloc>().add(FetchProfileEvent(userId: userId));
|
||||
}
|
||||
}
|
||||
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 12.w),
|
||||
child: CustomTextField(
|
||||
label: "First Name",
|
||||
hint: "Enter your first name",
|
||||
controller: firstNameController,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 12.w),
|
||||
child: CustomTextField(
|
||||
label: "Last Name",
|
||||
hint: "Enter your last name",
|
||||
controller: lastNameController,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 12.w),
|
||||
child: CustomTextField(
|
||||
label: "Email",
|
||||
hint: "Enter your email address",
|
||||
controller: emailController,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 12.w),
|
||||
child: CustomTextField(
|
||||
label: "Phone Number",
|
||||
hint: "Enter your phone number",
|
||||
controller: phoneController,
|
||||
),
|
||||
),
|
||||
void _populateFields(ProfileModel profile) {
|
||||
firstNameController.text = profile.firstName;
|
||||
lastNameController.text = profile.lastName;
|
||||
phoneController.text = profile.mobileNumber;
|
||||
address1Controller.text = profile.address1 ?? '';
|
||||
address2Controller.text = profile.address2 ?? '';
|
||||
}
|
||||
|
||||
SizedBox(height: 2.h),
|
||||
|
||||
// Location Details
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: CustomText(
|
||||
text: "Location Details",
|
||||
size: 18.sp,
|
||||
weight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 16.h),
|
||||
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 12.0.w),
|
||||
child: CustomTextField(
|
||||
label: "Address 1",
|
||||
hint: "Enter address manually or tap to search",
|
||||
controller: addressController,
|
||||
),
|
||||
),
|
||||
|
||||
SizedBox(height: 26.h),
|
||||
|
||||
// Buttons
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
Expanded(
|
||||
child: OutlinedButton(
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: const Color(0xFFF95F62),
|
||||
side: const BorderSide(color: Colors.transparent),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(38.r),
|
||||
),
|
||||
padding: EdgeInsets.symmetric(vertical: 12.h),
|
||||
),
|
||||
onPressed: () {},
|
||||
child: Text(
|
||||
"Cancel",
|
||||
style: TextStyle(
|
||||
fontSize: 16.sp,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 16.w),
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFFF95F62),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(38.r),
|
||||
),
|
||||
padding: EdgeInsets.symmetric(vertical: 6.h),
|
||||
),
|
||||
onPressed: () {},
|
||||
child: Text(
|
||||
"Save",
|
||||
style: TextStyle(
|
||||
fontSize: 16.sp,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 20.h),
|
||||
],
|
||||
void _saveProfile() async {
|
||||
if (_formKey.currentState?.validate() ?? false) {
|
||||
final userId = await LocalPreference.getUserId();
|
||||
if (userId != null) {
|
||||
// No setState here - BLoC will handle the state
|
||||
context.read<ProfileBloc>().add(
|
||||
UpdateProfileEvent(
|
||||
userId: userId,
|
||||
firstName: firstNameController.text.trim(),
|
||||
lastName: lastNameController.text.trim(),
|
||||
mobileNumber: phoneController.text.trim(),
|
||||
address1: address1Controller.text.trim().isEmpty
|
||||
? null
|
||||
: address1Controller.text.trim(),
|
||||
address2: address2Controller.text.trim().isEmpty
|
||||
? null
|
||||
: address2Controller.text.trim(),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
firstNameController.dispose();
|
||||
lastNameController.dispose();
|
||||
phoneController.dispose();
|
||||
address1Controller.dispose();
|
||||
address2Controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocConsumer<ProfileBloc, ProfileState>(
|
||||
listener: (context, state) {
|
||||
if (state is ProfileLoaded) {
|
||||
_populateFields(state.profile);
|
||||
} else if (state is ProfileUpdated) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(state.message),
|
||||
backgroundColor: Colors.green,
|
||||
duration: const Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
// Return true to indicate successful update
|
||||
Navigator.pop(context, true);
|
||||
} else if (state is ProfileError) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(state.message),
|
||||
backgroundColor: Colors.red,
|
||||
duration: const Duration(seconds: 3),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
builder: (context, state) {
|
||||
// Determine loading state from BLoC
|
||||
final isLoading = state is ProfileLoading || state is ProfileUpdating;
|
||||
final isInitialLoading = state is ProfileLoading;
|
||||
|
||||
// Show loading on initial fetch
|
||||
if (isInitialLoading) {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
body: Center(
|
||||
child: CircularProgressIndicator(
|
||||
color: Color(0xFFF95F62),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
body: SafeArea(
|
||||
child: Stack(
|
||||
children: [
|
||||
SingleChildScrollView(
|
||||
padding:
|
||||
EdgeInsets.symmetric(horizontal: 20.w, vertical: 10.h),
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
// Header
|
||||
CommonAppBar(
|
||||
isWhiteLogo: false,
|
||||
isProfilePage: true,
|
||||
showDivider: true,
|
||||
),
|
||||
|
||||
// Back + title
|
||||
backWidget(context, "Edit Profile", Colors.black),
|
||||
SizedBox(height: 33.h),
|
||||
|
||||
// Personal Details
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: CustomText(
|
||||
text: "Personal Details",
|
||||
size: 18.sp,
|
||||
weight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 16.h),
|
||||
|
||||
// First Name
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 12.w),
|
||||
child: CustomTextField(
|
||||
label: "First Name",
|
||||
hint: "Enter your first name",
|
||||
controller: firstNameController,
|
||||
enabled: !isLoading,
|
||||
validator: (value) {
|
||||
if (value == null || value.trim().isEmpty) {
|
||||
return 'First name is required';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
// Last Name
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 12.w),
|
||||
child: CustomTextField(
|
||||
label: "Last Name",
|
||||
hint: "Enter your last name",
|
||||
controller: lastNameController,
|
||||
enabled: !isLoading,
|
||||
validator: (value) {
|
||||
if (value == null || value.trim().isEmpty) {
|
||||
return 'Last name is required';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
// Phone Number
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 12.w),
|
||||
child: CustomTextField(
|
||||
label: "Phone Number",
|
||||
hint: "Enter your phone number",
|
||||
controller: phoneController,
|
||||
enabled: !isLoading,
|
||||
validator: (value) {
|
||||
if (value == null || value.trim().isEmpty) {
|
||||
return 'Phone number is required';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
SizedBox(height: 20.h),
|
||||
|
||||
// Location Details
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: CustomText(
|
||||
text: "Location Details",
|
||||
size: 18.sp,
|
||||
weight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 16.h),
|
||||
|
||||
// Address 1
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 12.0.w),
|
||||
child: CustomTextField(
|
||||
label: "Address 1",
|
||||
hint: "Enter address manually or tap to search",
|
||||
controller: address1Controller,
|
||||
enabled: !isLoading,
|
||||
),
|
||||
),
|
||||
|
||||
// Address 2
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 12.0.w),
|
||||
child: CustomTextField(
|
||||
label: "Address 2 (Optional)",
|
||||
hint: "Enter additional address details",
|
||||
controller: address2Controller,
|
||||
enabled: !isLoading,
|
||||
),
|
||||
),
|
||||
|
||||
SizedBox(height: 26.h),
|
||||
|
||||
// Buttons
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
Expanded(
|
||||
child: OutlinedButton(
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: const Color(0xFFF95F62),
|
||||
side: const BorderSide(
|
||||
color: Colors.transparent),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(38.r),
|
||||
),
|
||||
padding: EdgeInsets.symmetric(vertical: 12.h),
|
||||
),
|
||||
onPressed: isLoading
|
||||
? null
|
||||
: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: Text(
|
||||
"Cancel",
|
||||
style: TextStyle(
|
||||
fontSize: 16.sp,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 16.w),
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFFF95F62),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(38.r),
|
||||
),
|
||||
padding: EdgeInsets.symmetric(vertical: 6.h),
|
||||
),
|
||||
onPressed: isLoading ? null : _saveProfile,
|
||||
child: isLoading
|
||||
? SizedBox(
|
||||
height: 20.h,
|
||||
width: 20.w,
|
||||
child: CircularProgressIndicator(
|
||||
color: Colors.white,
|
||||
strokeWidth: 2,
|
||||
),
|
||||
)
|
||||
: Text(
|
||||
"Save",
|
||||
style: TextStyle(
|
||||
fontSize: 16.sp,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 20.h),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Loading overlay when saving
|
||||
if (state is ProfileUpdating)
|
||||
Container(
|
||||
color: Colors.black.withOpacity(0.3),
|
||||
child: Center(
|
||||
child: Card(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(20.w),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
CircularProgressIndicator(
|
||||
color: Color(0xFFF95F62),
|
||||
),
|
||||
SizedBox(height: 16.h),
|
||||
Text(
|
||||
'Updating profile...',
|
||||
style: TextStyle(
|
||||
fontSize: 14.sp,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'package:citycards_customer/itinerary_creation/views/magic_itinerary_view.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:citycards_customer/common_packages/custom_bottom_navbar.dart';
|
||||
@@ -6,6 +7,7 @@ import 'package:citycards_customer/itinerary_creation/views/itinerary_creation_s
|
||||
import 'package:citycards_customer/my_pass/views/my_pass_page_view.dart';
|
||||
import 'package:citycards_customer/postcard/views/postcard_initial_page_view.dart';
|
||||
import '../../common_bloc/bottom_navigation_bloc.dart';
|
||||
import '../../itinerary_creation/views/magic_itinerary_empty_view.dart';
|
||||
import 'registered_user_home_page.dart';
|
||||
|
||||
class HomePage extends StatefulWidget {
|
||||
@@ -38,7 +40,7 @@ class _HomePageState extends State<HomePage> {
|
||||
buildOffstageNavigator(
|
||||
1,
|
||||
currentIndex,
|
||||
const ItineraryCreationStartPage(),
|
||||
const MagicItineraryView(),
|
||||
_navigatorKeys[1],
|
||||
),
|
||||
buildOffstageNavigator(
|
||||
|
||||
@@ -5,6 +5,8 @@ import 'package:citycards_customer/core/route_constants.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
|
||||
import 'itinerary_creation_start_view.dart';
|
||||
|
||||
class MagicItineraryEmptyView extends StatelessWidget {
|
||||
const MagicItineraryEmptyView({super.key});
|
||||
|
||||
@@ -36,7 +38,12 @@ class MagicItineraryEmptyView extends StatelessWidget {
|
||||
SizedBox(height: 27.h),
|
||||
|
||||
CustomFilledButton(onTap: (){
|
||||
Navigator.pushNamed(context, RouteConstants.itineraryCreationStart);
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => ItineraryCreationStartPage(),
|
||||
),
|
||||
);
|
||||
},
|
||||
label: "Create My Itinerary", showArrow: true,)
|
||||
],
|
||||
|
||||
@@ -1,174 +0,0 @@
|
||||
import 'package:citycards_customer/common_packages/app_bar.dart';
|
||||
import 'package:citycards_customer/common_packages/custom_filled_button.dart';
|
||||
import 'package:citycards_customer/common_packages/custom_text.dart';
|
||||
import 'package:citycards_customer/core/route_constants.dart';
|
||||
import 'package:citycards_customer/postcard/widgets/dotted_border_container.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
|
||||
class MagicItineraryFilledView extends StatelessWidget {
|
||||
const MagicItineraryFilledView({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: Color(0xFFFFF5F5),
|
||||
body: SafeArea(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 10.h),
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
CommonAppBar(isWhiteLogo: false, isProfilePage: false, showDivider: false,),
|
||||
|
||||
SizedBox(height: 24.h),
|
||||
ItineraryFilledCard(),
|
||||
|
||||
SizedBox(height: 32.h),
|
||||
|
||||
CustomPaint(
|
||||
painter: DottedBorderPainter(),
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
padding: EdgeInsets.symmetric(vertical: 24.h),
|
||||
decoration: BoxDecoration(
|
||||
color: Color(0xFFF95F62).withOpacity(0.25),
|
||||
borderRadius: BorderRadius.circular(12.sp),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
CustomText(
|
||||
text: "Plan your next adventure",
|
||||
color: Color(0xFF656565),
|
||||
size: 14.sp,
|
||||
),
|
||||
SizedBox(height: 16.h),
|
||||
CustomFilledButton(
|
||||
onTap: () {
|
||||
Navigator.pushNamed(context, RouteConstants.itineraryCreationStart);
|
||||
},
|
||||
label: "Create My Itinerary",
|
||||
showArrow: true,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ItineraryFilledCard extends StatelessWidget {
|
||||
const ItineraryFilledCard({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 8.h),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Colors.black.withOpacity(0.12)),
|
||||
borderRadius: BorderRadius.circular(12.r),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
CustomText(
|
||||
text: "Melbourne Unlimited Card",
|
||||
size: 16.sp,
|
||||
weight: FontWeight.w500,
|
||||
),
|
||||
Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 2.h),
|
||||
decoration: BoxDecoration(
|
||||
color: Color(0xFF439F6E),
|
||||
borderRadius: BorderRadius.circular(100.r),
|
||||
),
|
||||
child: CustomText(
|
||||
text: "Active",
|
||||
size: 11.sp,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 4.h),
|
||||
|
||||
CustomText(
|
||||
text: "Melbourne",
|
||||
size: 12.sp,
|
||||
color: Colors.black.withOpacity(0.4),
|
||||
),
|
||||
SizedBox(height: 12.h),
|
||||
Row(
|
||||
children: [
|
||||
Image.asset("assets/icons/calender_filled.png", width: 16.sp),
|
||||
SizedBox(width: 4.w),
|
||||
CustomText(text: "7 days", color: Color(0xFF8E8E8E), size: 12.sp),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 8.h),
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.location_on_rounded,
|
||||
color: Color(0xFF8E8E8E),
|
||||
size: 16.sp,
|
||||
),
|
||||
SizedBox(width: 4.w),
|
||||
CustomText(
|
||||
text: "6 attractions",
|
||||
color: Color(0xFF8E8E8E),
|
||||
size: 12.sp,
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 8.h),
|
||||
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.watch_later, color: Color(0xFF8E8E8E), size: 16.sp),
|
||||
SizedBox(width: 4.w),
|
||||
CustomText(
|
||||
text: "Created 1/15/2024",
|
||||
color: Color(0xFF8E8E8E),
|
||||
size: 12.sp,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
SizedBox(height: 12.h),
|
||||
|
||||
InkWell(
|
||||
onTap: (){
|
||||
Navigator.of(context).pushReplacementNamed(RouteConstants.yourItinerary);
|
||||
},
|
||||
child: Container(
|
||||
height: 43.h,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(8.r),
|
||||
border: Border.all(color: Color(0xFFF95F62)),
|
||||
),
|
||||
child: Center(
|
||||
child: CustomText(
|
||||
text: "View Itinerary",
|
||||
size: 16.sp,
|
||||
color: Color(0xFFF95F62),
|
||||
weight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
273
lib/itinerary_creation/views/magic_itinerary_view.dart
Normal file
273
lib/itinerary_creation/views/magic_itinerary_view.dart
Normal file
@@ -0,0 +1,273 @@
|
||||
import 'package:citycards_customer/common_packages/app_bar.dart';
|
||||
import 'package:citycards_customer/common_packages/custom_filled_button.dart';
|
||||
import 'package:citycards_customer/common_packages/custom_text.dart';
|
||||
import 'package:citycards_customer/core/route_constants.dart';
|
||||
import 'package:citycards_customer/itinerary_creation/views/itinerary_creation_start_view.dart';
|
||||
import 'package:citycards_customer/postcard/widgets/dotted_border_container.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
|
||||
import '../../localPreference/local_preference.dart';
|
||||
import '../../login/view/login_email_bottomsheet.dart';
|
||||
|
||||
class MagicItineraryView extends StatefulWidget {
|
||||
const MagicItineraryView({super.key});
|
||||
|
||||
@override
|
||||
State<MagicItineraryView> createState() => _MagicItineraryViewState();
|
||||
}
|
||||
|
||||
class _MagicItineraryViewState extends State<MagicItineraryView> {
|
||||
bool isLoggedIn = false;
|
||||
bool isLoading = true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_checkLoginStatus();
|
||||
}
|
||||
|
||||
Future<void> _checkLoginStatus() async {
|
||||
final loginStatus = await LocalPreference.getLogin();
|
||||
setState(() {
|
||||
isLoggedIn = loginStatus;
|
||||
isLoading = false;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: Color(0xFFFFF5F5),
|
||||
body: SafeArea(
|
||||
child: isLoading
|
||||
? Center(child: CircularProgressIndicator())
|
||||
: Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 10.h),
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
CommonAppBar(
|
||||
isWhiteLogo: false,
|
||||
isProfilePage: false,
|
||||
showDivider: false,
|
||||
),
|
||||
SizedBox(height: 24.h),
|
||||
|
||||
// Show different UI based on login status
|
||||
if (isLoggedIn) ...[
|
||||
ItineraryFilledCard(),
|
||||
SizedBox(height: 32.h),
|
||||
CustomPaint(
|
||||
painter: DottedBorderPainter(),
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
padding: EdgeInsets.symmetric(vertical: 24.h),
|
||||
decoration: BoxDecoration(
|
||||
color: Color(0xFFF95F62).withOpacity(0.25),
|
||||
borderRadius: BorderRadius.circular(12.sp),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
CustomText(
|
||||
text: "Plan your next adventure",
|
||||
color: Color(0xFF656565),
|
||||
size: 14.sp,
|
||||
),
|
||||
SizedBox(height: 16.h),
|
||||
CustomFilledButton(
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) =>
|
||||
ItineraryCreationStartPage(),
|
||||
),
|
||||
);
|
||||
},
|
||||
label: "Create My Itinerary",
|
||||
showArrow: true,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
] else ...[
|
||||
EmptyItineraryView(),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class EmptyItineraryView extends StatelessWidget {
|
||||
const EmptyItineraryView({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
SizedBox(height: 40.h),
|
||||
|
||||
// Illustration image - replace with your asset path
|
||||
Image.asset(
|
||||
"assets/images/not_login.png", // Replace with your actual asset path
|
||||
height: 300.h,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
|
||||
SizedBox(height: 32.h),
|
||||
|
||||
CustomText(
|
||||
text: "You have not Logged in Yet! ☹️",
|
||||
size: 18.sp,
|
||||
weight: FontWeight.w600,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
|
||||
SizedBox(height: 12.h),
|
||||
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 24.w),
|
||||
child: CustomText(
|
||||
text: "Log in or purchase a pass to unlock the magic itinerary!",
|
||||
size: 14.sp,
|
||||
color: Color(0xFF656565),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
|
||||
SizedBox(height: 32.h),
|
||||
|
||||
CustomFilledButton(
|
||||
onTap: () {
|
||||
showModalBottomSheet(
|
||||
backgroundColor: Colors.white,
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(
|
||||
top: Radius.circular(12.r),
|
||||
),
|
||||
),
|
||||
builder: (_) => const LoginEmailBottomsheet(),
|
||||
);
|
||||
},
|
||||
label: "Log In",
|
||||
showArrow: true,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ItineraryFilledCard extends StatelessWidget {
|
||||
const ItineraryFilledCard({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 8.h),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Colors.black.withOpacity(0.12)),
|
||||
borderRadius: BorderRadius.circular(12.r),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
CustomText(
|
||||
text: "Melbourne Unlimited Card",
|
||||
size: 16.sp,
|
||||
weight: FontWeight.w500,
|
||||
),
|
||||
Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 2.h),
|
||||
decoration: BoxDecoration(
|
||||
color: Color(0xFF439F6E),
|
||||
borderRadius: BorderRadius.circular(100.r),
|
||||
),
|
||||
child: CustomText(
|
||||
text: "Active",
|
||||
size: 11.sp,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 4.h),
|
||||
CustomText(
|
||||
text: "Melbourne",
|
||||
size: 12.sp,
|
||||
color: Colors.black.withOpacity(0.4),
|
||||
),
|
||||
SizedBox(height: 12.h),
|
||||
Row(
|
||||
children: [
|
||||
Image.asset("assets/icons/calender_filled.png", width: 16.sp),
|
||||
SizedBox(width: 4.w),
|
||||
CustomText(text: "7 days", color: Color(0xFF8E8E8E), size: 12.sp),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 8.h),
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.location_on_rounded,
|
||||
color: Color(0xFF8E8E8E),
|
||||
size: 16.sp,
|
||||
),
|
||||
SizedBox(width: 4.w),
|
||||
CustomText(
|
||||
text: "6 attractions",
|
||||
color: Color(0xFF8E8E8E),
|
||||
size: 12.sp,
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 8.h),
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.watch_later, color: Color(0xFF8E8E8E), size: 16.sp),
|
||||
SizedBox(width: 4.w),
|
||||
CustomText(
|
||||
text: "Created 1/15/2024",
|
||||
color: Color(0xFF8E8E8E),
|
||||
size: 12.sp,
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 12.h),
|
||||
InkWell(
|
||||
onTap: () {
|
||||
Navigator.of(context)
|
||||
.pushReplacementNamed(RouteConstants.yourItinerary);
|
||||
},
|
||||
child: Container(
|
||||
height: 43.h,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(8.r),
|
||||
border: Border.all(color: Color(0xFFF95F62)),
|
||||
),
|
||||
child: Center(
|
||||
child: CustomText(
|
||||
text: "View Itinerary",
|
||||
size: 16.sp,
|
||||
color: Color(0xFFF95F62),
|
||||
weight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -56,6 +56,22 @@ class LocalDatabase {
|
||||
refresh_token_max_age INTEGER NOT NULL
|
||||
)
|
||||
''');
|
||||
|
||||
/// USER DETAILS TABLE
|
||||
await db.execute('''
|
||||
CREATE TABLE user_details (
|
||||
id INTEGER PRIMARY KEY,
|
||||
user_id INTEGER NOT NULL,
|
||||
first_name TEXT NOT NULL,
|
||||
last_name TEXT NOT NULL,
|
||||
full_name TEXT NOT NULL,
|
||||
email_address TEXT NOT NULL,
|
||||
role TEXT NOT NULL,
|
||||
role_id INTEGER NOT NULL
|
||||
)
|
||||
''');
|
||||
|
||||
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -141,6 +141,18 @@ class LocalPreference {
|
||||
);
|
||||
}
|
||||
|
||||
/// Update only access token (for refresh token flow)
|
||||
static Future<void> setAccessToken(String accessToken) async {
|
||||
final db = await LocalDatabase().database;
|
||||
|
||||
await db.update(
|
||||
'user_tokens',
|
||||
{'access_token': accessToken},
|
||||
where: 'id = ?',
|
||||
whereArgs: [1],
|
||||
);
|
||||
}
|
||||
|
||||
/// Get access token
|
||||
static Future<String?> getAccessToken() async {
|
||||
final db = await LocalDatabase().database;
|
||||
@@ -184,4 +196,49 @@ class LocalPreference {
|
||||
);
|
||||
}
|
||||
|
||||
/// Set user details
|
||||
static Future<void> setUserDetails({
|
||||
required int userId,
|
||||
required String firstName,
|
||||
required String lastName,
|
||||
required String fullName,
|
||||
required String emailAddress,
|
||||
required String role,
|
||||
required int roleId,
|
||||
}) async {
|
||||
final db = await LocalDatabase().database;
|
||||
|
||||
await db.insert(
|
||||
'user_details',
|
||||
{
|
||||
'id': 1,
|
||||
'user_id': userId,
|
||||
'first_name': firstName,
|
||||
'last_name': lastName,
|
||||
'full_name': fullName,
|
||||
'email_address': emailAddress,
|
||||
'role': role,
|
||||
'role_id': roleId,
|
||||
},
|
||||
conflictAlgorithm: ConflictAlgorithm.replace,
|
||||
);
|
||||
}
|
||||
|
||||
/// Get userId
|
||||
static Future<int?> getUserId() async {
|
||||
final db = await LocalDatabase().database;
|
||||
|
||||
final result = await db.query(
|
||||
'user_details',
|
||||
where: 'id = ?',
|
||||
whereArgs: [1],
|
||||
);
|
||||
|
||||
if (result.isNotEmpty) {
|
||||
return result.first['user_id'] as int;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -32,6 +32,15 @@ class VerifyOtpBloc extends Bloc<VerifyOtpEvent, VerifyOtpState> {
|
||||
refreshToken: userModel.refreshToken,
|
||||
refreshTokenMaxAge: userModel.refreshTokenMaxAge,
|
||||
);
|
||||
await LocalPreference.setUserDetails(
|
||||
userId: userModel.user.id,
|
||||
firstName: userModel.user.firstName,
|
||||
lastName: userModel.user.lastName,
|
||||
fullName: userModel.user.fullName,
|
||||
emailAddress: userModel.user.emailAddress,
|
||||
role: userModel.user.role,
|
||||
roleId: userModel.user.roleId,
|
||||
);
|
||||
emit(VerifyOtpSuccess(response: userModel));
|
||||
} catch (e) {
|
||||
emit(VerifyOtpError(errorMessage: e.toString()));
|
||||
|
||||
@@ -15,6 +15,7 @@ import 'home/repository/home_repository.dart';
|
||||
import 'login/bloc/login/login_bloc.dart';
|
||||
import 'login/repository/login_repository.dart';
|
||||
import 'my_pass/blocs/my_pass_bloc.dart';
|
||||
import 'profile/bloc/profile/profile_bloc.dart';
|
||||
|
||||
void main() {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
@@ -60,6 +61,7 @@ class MyApp extends StatelessWidget {
|
||||
loginRepository: LoginRepository(),
|
||||
),
|
||||
),
|
||||
BlocProvider(create: (context) => ProfileBloc()),
|
||||
],
|
||||
child: MaterialApp(
|
||||
onGenerateRoute: _appRouter.onGenerateRoute,
|
||||
|
||||
@@ -11,6 +11,7 @@ class ApiUrls {
|
||||
static const attractionDetails = "$baseUrl/mobile/list";
|
||||
static const home = "$baseUrl/mobile";
|
||||
static const faqPrivacyTerms = "$baseUrl/mobile/user/cms-data";
|
||||
static const userProfile = "$baseUrl/mobile/user";
|
||||
|
||||
|
||||
//Post Apis
|
||||
|
||||
@@ -187,21 +187,12 @@ class NetworkApiService {
|
||||
|
||||
final response = await _dio.post(
|
||||
ApiUrls.refreshToken,
|
||||
data: {
|
||||
"refreshToken": refreshToken,
|
||||
},
|
||||
data: {"refreshToken": refreshToken},
|
||||
options: Options(
|
||||
headers: {
|
||||
'Authorization': null,
|
||||
},
|
||||
headers: {'Authorization': null},
|
||||
),
|
||||
);
|
||||
|
||||
await LocalPreference.setTokens(
|
||||
accessToken: response.data['accessToken'],
|
||||
refreshToken: response.data['refreshToken'],
|
||||
refreshTokenMaxAge: response.data['refreshTokenMaxAge'],
|
||||
);
|
||||
await LocalPreference.setAccessToken(response.data['accessToken']);
|
||||
|
||||
return true;
|
||||
} catch (_) {
|
||||
|
||||
81
lib/profile/bloc/profile/profile_bloc.dart
Normal file
81
lib/profile/bloc/profile/profile_bloc.dart
Normal file
@@ -0,0 +1,81 @@
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import '../../repository/profile_repository.dart';
|
||||
import 'profile_event.dart';
|
||||
import 'profile_state.dart';
|
||||
|
||||
class ProfileBloc extends Bloc<ProfileEvent, ProfileState> {
|
||||
final ProfileRepository _profileRepository;
|
||||
|
||||
ProfileBloc({ProfileRepository? profileRepository})
|
||||
: _profileRepository = profileRepository ?? ProfileRepository(),
|
||||
super(const ProfileInitial()) {
|
||||
on<FetchProfileEvent>(_onFetchProfile);
|
||||
on<UpdateProfileEvent>(_onUpdateProfile);
|
||||
on<ResetProfileEvent>(_onResetProfile);
|
||||
}
|
||||
|
||||
/// Handle fetching user profile
|
||||
Future<void> _onFetchProfile(
|
||||
FetchProfileEvent event,
|
||||
Emitter<ProfileState> emit,
|
||||
) async {
|
||||
try {
|
||||
emit(const ProfileLoading());
|
||||
|
||||
final profile = await _profileRepository.fetchUserProfile();
|
||||
|
||||
emit(ProfileLoaded(profile: profile));
|
||||
|
||||
if (kDebugMode) {
|
||||
print(
|
||||
'✅ Profile fetched successfully: ${profile.firstName} ${profile.lastName}',
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
final errorMessage = e.toString();
|
||||
emit(ProfileError(message: errorMessage));
|
||||
|
||||
if (kDebugMode) {
|
||||
print('❌ Error fetching profile: $errorMessage');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle updating user profile
|
||||
Future<void> _onUpdateProfile(
|
||||
UpdateProfileEvent event,
|
||||
Emitter<ProfileState> emit,
|
||||
) async {
|
||||
try {
|
||||
emit(const ProfileUpdating());
|
||||
|
||||
final updatedProfile = await _profileRepository.updateUserProfile(
|
||||
data: event.toJson(),
|
||||
);
|
||||
|
||||
emit(ProfileUpdated(profile: updatedProfile));
|
||||
|
||||
if (kDebugMode) {
|
||||
print(
|
||||
'✅ Profile updated successfully: ${updatedProfile.firstName} ${updatedProfile.lastName}',
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
final errorMessage = e.toString();
|
||||
emit(ProfileError(message: errorMessage));
|
||||
|
||||
if (kDebugMode) {
|
||||
print('❌ Error updating profile: $errorMessage');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle resetting profile state
|
||||
void _onResetProfile(
|
||||
ResetProfileEvent event,
|
||||
Emitter<ProfileState> emit,
|
||||
) {
|
||||
emit(const ProfileInitial());
|
||||
}
|
||||
}
|
||||
62
lib/profile/bloc/profile/profile_event.dart
Normal file
62
lib/profile/bloc/profile/profile_event.dart
Normal file
@@ -0,0 +1,62 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
abstract class ProfileEvent extends Equatable {
|
||||
const ProfileEvent();
|
||||
|
||||
@override
|
||||
List<Object?> get props => [];
|
||||
}
|
||||
|
||||
/// Event to fetch user profile
|
||||
class FetchProfileEvent extends ProfileEvent {
|
||||
final int userId;
|
||||
|
||||
const FetchProfileEvent({required this.userId});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [userId];
|
||||
}
|
||||
|
||||
/// Event to update user profile
|
||||
class UpdateProfileEvent extends ProfileEvent {
|
||||
final int userId;
|
||||
final String firstName;
|
||||
final String lastName;
|
||||
final String mobileNumber;
|
||||
final String? address1;
|
||||
final String? address2;
|
||||
|
||||
const UpdateProfileEvent({
|
||||
required this.userId,
|
||||
required this.firstName,
|
||||
required this.lastName,
|
||||
required this.mobileNumber,
|
||||
this.address1,
|
||||
this.address2,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
userId,
|
||||
firstName,
|
||||
lastName,
|
||||
mobileNumber,
|
||||
address1,
|
||||
address2,
|
||||
];
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'firstName': firstName,
|
||||
'lastName': lastName,
|
||||
'mobileNumber': mobileNumber,
|
||||
if (address1 != null && address1!.isNotEmpty) 'address1': address1,
|
||||
if (address2 != null && address2!.isNotEmpty) 'address2': address2,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Event to reset profile state
|
||||
class ResetProfileEvent extends ProfileEvent {
|
||||
const ResetProfileEvent();
|
||||
}
|
||||
58
lib/profile/bloc/profile/profile_state.dart
Normal file
58
lib/profile/bloc/profile/profile_state.dart
Normal file
@@ -0,0 +1,58 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import '../../models/profile_model.dart';
|
||||
|
||||
abstract class ProfileState extends Equatable {
|
||||
const ProfileState();
|
||||
|
||||
@override
|
||||
List<Object?> get props => [];
|
||||
}
|
||||
|
||||
/// Initial state
|
||||
class ProfileInitial extends ProfileState {
|
||||
const ProfileInitial();
|
||||
}
|
||||
|
||||
/// Loading state for fetching profile
|
||||
class ProfileLoading extends ProfileState {
|
||||
const ProfileLoading();
|
||||
}
|
||||
|
||||
/// Success state when profile is fetched
|
||||
class ProfileLoaded extends ProfileState {
|
||||
final ProfileModel profile;
|
||||
|
||||
const ProfileLoaded({required this.profile});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [profile];
|
||||
}
|
||||
|
||||
/// Loading state for updating profile
|
||||
class ProfileUpdating extends ProfileState {
|
||||
const ProfileUpdating();
|
||||
}
|
||||
|
||||
/// Success state when profile is updated
|
||||
class ProfileUpdated extends ProfileState {
|
||||
final ProfileModel profile;
|
||||
final String message;
|
||||
|
||||
const ProfileUpdated({
|
||||
required this.profile,
|
||||
this.message = 'Profile updated successfully',
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [profile, message];
|
||||
}
|
||||
|
||||
/// Error state
|
||||
class ProfileError extends ProfileState {
|
||||
final String message;
|
||||
|
||||
const ProfileError({required this.message});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [message];
|
||||
}
|
||||
171
lib/profile/models/profile_model.dart
Normal file
171
lib/profile/models/profile_model.dart
Normal file
@@ -0,0 +1,171 @@
|
||||
class ProfileModel {
|
||||
final int id;
|
||||
final String firstName;
|
||||
final String lastName;
|
||||
final int roleXid;
|
||||
final String emailAddress;
|
||||
final String isdCode;
|
||||
final String mobileNumber;
|
||||
final String? address1;
|
||||
final String? address2;
|
||||
final String? cityName;
|
||||
final String? zipCode;
|
||||
final String? stateName;
|
||||
final String? country;
|
||||
final String? timezone;
|
||||
final String? lastLogin;
|
||||
final bool isActive;
|
||||
final String createdAt;
|
||||
final String updatedAt;
|
||||
final RoleModel? role;
|
||||
|
||||
ProfileModel({
|
||||
required this.id,
|
||||
required this.firstName,
|
||||
required this.lastName,
|
||||
required this.roleXid,
|
||||
required this.emailAddress,
|
||||
required this.isdCode,
|
||||
required this.mobileNumber,
|
||||
this.address1,
|
||||
this.address2,
|
||||
this.cityName,
|
||||
this.zipCode,
|
||||
this.stateName,
|
||||
this.country,
|
||||
this.timezone,
|
||||
this.lastLogin,
|
||||
required this.isActive,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
this.role,
|
||||
});
|
||||
|
||||
factory ProfileModel.fromJson(Map<String, dynamic> json) {
|
||||
return ProfileModel(
|
||||
id: json['id'] ?? 0,
|
||||
firstName: json['firstName'] ?? 'N/A',
|
||||
lastName: json['lastName'] ?? 'N/A',
|
||||
roleXid: json['roleXid'] ?? 0,
|
||||
emailAddress: json['emailAddress'] ?? 'N/A',
|
||||
isdCode: json['isdCode'] ?? 'N/A',
|
||||
mobileNumber: json['mobileNumber'] ?? 'N/A',
|
||||
address1: json['address1'],
|
||||
address2: json['address2'],
|
||||
cityName: json['cityName'],
|
||||
zipCode: json['zipCode'],
|
||||
stateName: json['stateName'],
|
||||
country: json['country'],
|
||||
timezone: json['timezone'],
|
||||
lastLogin: json['lastLogin'],
|
||||
isActive: json['isActive'] ?? false,
|
||||
createdAt: json['createdAt'] ?? 'N/A',
|
||||
updatedAt: json['updatedAt'] ?? 'N/A',
|
||||
role: json['role'] != null ? RoleModel.fromJson(json['role']) : null,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'firstName': firstName,
|
||||
'lastName': lastName,
|
||||
'roleXid': roleXid,
|
||||
'emailAddress': emailAddress,
|
||||
'isdCode': isdCode,
|
||||
'mobileNumber': mobileNumber,
|
||||
'address1': address1,
|
||||
'address2': address2,
|
||||
'cityName': cityName,
|
||||
'zipCode': zipCode,
|
||||
'stateName': stateName,
|
||||
'country': country,
|
||||
'timezone': timezone,
|
||||
'lastLogin': lastLogin,
|
||||
'isActive': isActive,
|
||||
'createdAt': createdAt,
|
||||
'updatedAt': updatedAt,
|
||||
if (role != null) 'role': role!.toJson(),
|
||||
};
|
||||
}
|
||||
|
||||
ProfileModel copyWith({
|
||||
int? id,
|
||||
String? firstName,
|
||||
String? lastName,
|
||||
int? roleXid,
|
||||
String? emailAddress,
|
||||
String? isdCode,
|
||||
String? mobileNumber,
|
||||
String? address1,
|
||||
String? address2,
|
||||
String? cityName,
|
||||
String? zipCode,
|
||||
String? stateName,
|
||||
String? country,
|
||||
String? timezone,
|
||||
String? lastLogin,
|
||||
bool? isActive,
|
||||
String? createdAt,
|
||||
String? updatedAt,
|
||||
RoleModel? role,
|
||||
}) {
|
||||
return ProfileModel(
|
||||
id: id ?? this.id,
|
||||
firstName: firstName ?? this.firstName,
|
||||
lastName: lastName ?? this.lastName,
|
||||
roleXid: roleXid ?? this.roleXid,
|
||||
emailAddress: emailAddress ?? this.emailAddress,
|
||||
isdCode: isdCode ?? this.isdCode,
|
||||
mobileNumber: mobileNumber ?? this.mobileNumber,
|
||||
address1: address1 ?? this.address1,
|
||||
address2: address2 ?? this.address2,
|
||||
cityName: cityName ?? this.cityName,
|
||||
zipCode: zipCode ?? this.zipCode,
|
||||
stateName: stateName ?? this.stateName,
|
||||
country: country ?? this.country,
|
||||
timezone: timezone ?? this.timezone,
|
||||
lastLogin: lastLogin ?? this.lastLogin,
|
||||
isActive: isActive ?? this.isActive,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
updatedAt: updatedAt ?? this.updatedAt,
|
||||
role: role ?? this.role,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class RoleModel {
|
||||
final int id;
|
||||
final String name;
|
||||
final bool isActive;
|
||||
final String createdAt;
|
||||
final String updatedAt;
|
||||
|
||||
RoleModel({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.isActive,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
});
|
||||
|
||||
factory RoleModel.fromJson(Map<String, dynamic> json) {
|
||||
return RoleModel(
|
||||
id: json['id'] ?? 0,
|
||||
name: json['name'] ?? 'N/A',
|
||||
isActive: json['isActive'] ?? false,
|
||||
createdAt: json['createdAt'] ?? 'N/A',
|
||||
updatedAt: json['updatedAt'] ?? 'N/A',
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'name': name,
|
||||
'isActive': isActive,
|
||||
'createdAt': createdAt,
|
||||
'updatedAt': updatedAt,
|
||||
};
|
||||
}
|
||||
}
|
||||
33
lib/profile/repository/profile_repository.dart
Normal file
33
lib/profile/repository/profile_repository.dart
Normal file
@@ -0,0 +1,33 @@
|
||||
import '../models/profile_model.dart';
|
||||
import '../../networkApiServices/network_api_services.dart';
|
||||
import '../../networkApiServices/api_urls.dart';
|
||||
import '../../localPreference/local_preference.dart';
|
||||
|
||||
class ProfileRepository {
|
||||
final NetworkApiService _apiService = NetworkApiService();
|
||||
|
||||
/// Fetch user profile (userId from local storage)
|
||||
Future<ProfileModel> fetchUserProfile() async {
|
||||
final int? userId = await LocalPreference.getUserId();
|
||||
|
||||
final response = await _apiService.getApi(
|
||||
url: '${ApiUrls.userProfile}/$userId',
|
||||
);
|
||||
|
||||
return ProfileModel.fromJson(response.data);
|
||||
}
|
||||
|
||||
/// Update user profile (userId from local storage)
|
||||
Future<ProfileModel> updateUserProfile({
|
||||
required Map<String, dynamic> data,
|
||||
}) async {
|
||||
final int? userId = await LocalPreference.getUserId();
|
||||
|
||||
final response = await _apiService.putApi(
|
||||
url: '${ApiUrls.userProfile}/$userId',
|
||||
data: data,
|
||||
);
|
||||
|
||||
return ProfileModel.fromJson(response.data);
|
||||
}
|
||||
}
|
||||
@@ -7,12 +7,56 @@ import 'package:citycards_customer/core/route_constants.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import '../../localPreference/local_preference.dart';
|
||||
import '../../login/view/login_email_bottomsheet.dart';
|
||||
import '../bloc/profile/profile_bloc.dart';
|
||||
import '../bloc/profile/profile_event.dart';
|
||||
import '../bloc/profile/profile_state.dart';
|
||||
import '../models/profile_model.dart';
|
||||
|
||||
class ProfilePage extends StatelessWidget {
|
||||
class ProfilePage extends StatefulWidget {
|
||||
const ProfilePage({super.key});
|
||||
|
||||
@override
|
||||
State<ProfilePage> createState() => _ProfilePageState();
|
||||
}
|
||||
|
||||
class _ProfilePageState extends State<ProfilePage> {
|
||||
bool isLogin = false;
|
||||
bool isLoading = true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_checkLoginStatus();
|
||||
}
|
||||
|
||||
Future<void> _checkLoginStatus() async {
|
||||
final loginStatus = await LocalPreference.getLogin();
|
||||
final userId = await LocalPreference.getUserId();
|
||||
|
||||
setState(() {
|
||||
isLogin = loginStatus;
|
||||
isLoading = false;
|
||||
});
|
||||
|
||||
// Fetch profile data if user is logged in
|
||||
if (loginStatus && userId != null) {
|
||||
context.read<ProfileBloc>().add(FetchProfileEvent(userId: userId));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (isLoading) {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
body: Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
body: SafeArea(
|
||||
@@ -21,94 +65,89 @@ class ProfilePage extends StatelessWidget {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
CommonAppBar(isWhiteLogo: false, isProfilePage: true, showDivider: true,),
|
||||
backWidget(context,"My Profile", Colors.black),
|
||||
CommonAppBar(
|
||||
isWhiteLogo: false,
|
||||
isProfilePage: true,
|
||||
showDivider: true,
|
||||
),
|
||||
backWidget(context, "My Profile", Colors.black),
|
||||
|
||||
SizedBox(height: 29.h),
|
||||
// Profile Image and Name
|
||||
Row(
|
||||
children: [
|
||||
CircleAvatar(
|
||||
radius: 38.r,
|
||||
backgroundImage: AssetImage(
|
||||
"assets/images/profile_img.png",
|
||||
),
|
||||
),
|
||||
SizedBox(width: 16.w),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Laysha Adams',
|
||||
style: TextStyle(
|
||||
fontSize: 16.sp,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
// SizedBox(height: 4,),
|
||||
Row(
|
||||
|
||||
// Show different UI based on login status
|
||||
if (!isLogin) ...[
|
||||
// Guest User UI
|
||||
_buildGuestUI(context),
|
||||
] else ...[
|
||||
// Logged In User UI with BLoC
|
||||
BlocBuilder<ProfileBloc, ProfileState>(
|
||||
builder: (context, state) {
|
||||
if (state is ProfileLoading) {
|
||||
return Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
} else if (state is ProfileLoaded) {
|
||||
return _buildLoggedInUI(context, state.profile);
|
||||
} else if (state is ProfileUpdated) {
|
||||
return _buildLoggedInUI(context, state.profile);
|
||||
} else if (state is ProfileError) {
|
||||
return Column(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.location_on_sharp,
|
||||
color: Color(0xFF8E8E8E),
|
||||
size: 14.sp,
|
||||
Icons.error_outline,
|
||||
color: Colors.red,
|
||||
size: 48.sp,
|
||||
),
|
||||
SizedBox(width: 4.w),
|
||||
SizedBox(height: 16.h),
|
||||
Text(
|
||||
'Louisiana, United States',
|
||||
'Failed to load profile',
|
||||
style: TextStyle(
|
||||
fontSize: 12.sp,
|
||||
fontSize: 16.sp,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 8.h),
|
||||
Text(
|
||||
state.message,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 14.sp,
|
||||
color: Color(0xFF8E8E8E),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 16.h),
|
||||
ElevatedButton(
|
||||
onPressed: () async {
|
||||
final userId = await LocalPreference.getUserId();
|
||||
if (userId != null) {
|
||||
context.read<ProfileBloc>().add(
|
||||
FetchProfileEvent(userId: userId),
|
||||
);
|
||||
}
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Color(0xFFF95F62),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(38.r),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
'Retry',
|
||||
style: TextStyle(color: Colors.white),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
// Default fallback
|
||||
return _buildLoggedInUI(context, null);
|
||||
},
|
||||
),
|
||||
],
|
||||
|
||||
SizedBox(height: 30.h),
|
||||
|
||||
// Account Settings Section
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: CustomText(
|
||||
text: "Account Settings",
|
||||
weight: FontWeight.w500,
|
||||
size: 18.sp,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 10.h),
|
||||
|
||||
_buildListTile(
|
||||
icon: "assets/icons/user_profile.png",
|
||||
title: 'Edit profile',
|
||||
onTap: () {
|
||||
Navigator.pushNamed(context, RouteConstants.editProfile);
|
||||
},
|
||||
),
|
||||
_buildListTile(
|
||||
icon: "assets/icons/change_language.png",
|
||||
title: 'Change language',
|
||||
onTap: () {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
backgroundColor: Colors.white,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(
|
||||
top: Radius.circular(12.r),
|
||||
),
|
||||
),
|
||||
builder: (context) => BlocProvider(
|
||||
create: (_)=> LanguageBloc(),
|
||||
child: LanguageSelectionBottomsheet()),
|
||||
);
|
||||
},
|
||||
),
|
||||
SizedBox(height: 24.h),
|
||||
|
||||
// Support & Legal Section
|
||||
// Support & Legal Section (Always visible)
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: CustomText(
|
||||
@@ -153,25 +192,36 @@ class ProfilePage extends StatelessWidget {
|
||||
|
||||
SizedBox(height: 22.h),
|
||||
|
||||
// Logout Button
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: OutlinedButton(
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: Color(0xFFF95F62),
|
||||
side: const BorderSide(color: Color(0xFFF95F62)),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(38.r),
|
||||
// Logout Button (Only for logged in users)
|
||||
if (isLogin)
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: OutlinedButton(
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: Color(0xFFF95F62),
|
||||
side: const BorderSide(color: Color(0xFFF95F62)),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(38.r),
|
||||
),
|
||||
padding: EdgeInsets.symmetric(vertical: 6.h),
|
||||
),
|
||||
onPressed: () async {
|
||||
// Handle logout
|
||||
// await LocalPreference.clearPreference();
|
||||
context.read<ProfileBloc>().add(ResetProfileEvent());
|
||||
setState(() {
|
||||
isLogin = false;
|
||||
});
|
||||
},
|
||||
child: Text(
|
||||
'Log out',
|
||||
style: TextStyle(
|
||||
fontSize: 16.sp,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
padding: EdgeInsets.symmetric(vertical: 6.h),
|
||||
),
|
||||
onPressed: () {},
|
||||
child: Text(
|
||||
'Log out',
|
||||
style: TextStyle(fontSize: 16.sp, fontWeight: FontWeight.w600),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -179,6 +229,224 @@ class ProfilePage extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
// Guest User UI (Not logged in)
|
||||
Widget _buildGuestUI(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
// Greeting Text
|
||||
Text(
|
||||
'Hey, Stranger! 👋',
|
||||
style: TextStyle(
|
||||
fontSize: 20.sp,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.black,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 8.h),
|
||||
Text(
|
||||
'We are thrilled to have you on our app.',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 13.sp,
|
||||
color: Color(0xFF8E8E8E),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'Why not make it official?',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 13.sp,
|
||||
color: Color(0xFF8E8E8E),
|
||||
),
|
||||
),
|
||||
|
||||
SizedBox(height: 24.h),
|
||||
|
||||
// Illustration Image
|
||||
Image.asset(
|
||||
"assets/images/guest_illustration.png",
|
||||
height: 200.h,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
|
||||
SizedBox(height: 32.h),
|
||||
|
||||
// Sign In Button
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Color(0xFFF95F62),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(38.r),
|
||||
),
|
||||
padding: EdgeInsets.symmetric(vertical: 12.h),
|
||||
),
|
||||
onPressed: () {
|
||||
showModalBottomSheet(
|
||||
backgroundColor: Colors.white,
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(
|
||||
top: Radius.circular(12.r),
|
||||
),
|
||||
),
|
||||
builder: (_) => const LoginEmailBottomsheet(),
|
||||
);
|
||||
},
|
||||
child: Text(
|
||||
'Sign in',
|
||||
style: TextStyle(
|
||||
fontSize: 16.sp,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// Logged In User UI with dynamic data from API
|
||||
Widget _buildLoggedInUI(BuildContext context, ProfileModel? profile) {
|
||||
// Construct full name
|
||||
String fullName = 'User';
|
||||
if (profile != null) {
|
||||
fullName = '${profile.firstName} ${profile.lastName}'.trim();
|
||||
if (fullName.isEmpty) {
|
||||
fullName = 'User';
|
||||
}
|
||||
}
|
||||
|
||||
// Construct location
|
||||
String location = 'Not specified';
|
||||
if (profile != null) {
|
||||
List<String> locationParts = [];
|
||||
|
||||
if (profile.address1 != null && profile.address1!.isNotEmpty) {
|
||||
locationParts.add(profile.address1!);
|
||||
}
|
||||
if (profile.address2 != null && profile.address2!.isNotEmpty) {
|
||||
locationParts.add(profile.address2!);
|
||||
}
|
||||
|
||||
if (locationParts.isNotEmpty) {
|
||||
location = locationParts.join(', ');
|
||||
}
|
||||
}
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
// Profile Image and Name
|
||||
Row(
|
||||
children: [
|
||||
CircleAvatar(
|
||||
radius: 38.r,
|
||||
backgroundImage: AssetImage(
|
||||
"assets/images/profile_img.png",
|
||||
),
|
||||
),
|
||||
SizedBox(width: 16.w),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
fullName,
|
||||
style: TextStyle(
|
||||
fontSize: 16.sp,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
SizedBox(height: 4.h),
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.location_on_sharp,
|
||||
color: Color(0xFF8E8E8E),
|
||||
size: 14.sp,
|
||||
),
|
||||
SizedBox(width: 4.w),
|
||||
Expanded(
|
||||
child: Text(
|
||||
location,
|
||||
style: TextStyle(
|
||||
fontSize: 12.sp,
|
||||
color: Color(0xFF8E8E8E),
|
||||
),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
SizedBox(height: 30.h),
|
||||
|
||||
// Account Settings Section
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: CustomText(
|
||||
text: "Account Settings",
|
||||
weight: FontWeight.w500,
|
||||
size: 18.sp,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 10.h),
|
||||
|
||||
_buildListTile(
|
||||
icon: "assets/icons/user_profile.png",
|
||||
title: 'Edit profile',
|
||||
onTap: () async {
|
||||
final result = await Navigator.pushNamed(
|
||||
context,
|
||||
RouteConstants.editProfile,
|
||||
);
|
||||
|
||||
// Refresh profile if edit was successful
|
||||
if (result == true) {
|
||||
final userId = await LocalPreference.getUserId();
|
||||
if (userId != null) {
|
||||
context.read<ProfileBloc>().add(
|
||||
FetchProfileEvent(userId: userId),
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
_buildListTile(
|
||||
icon: "assets/icons/change_language.png",
|
||||
title: 'Change language',
|
||||
onTap: () {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
backgroundColor: Colors.white,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(
|
||||
top: Radius.circular(12.r),
|
||||
),
|
||||
),
|
||||
builder: (context) => BlocProvider(
|
||||
create: (_) => LanguageBloc(),
|
||||
child: LanguageSelectionBottomsheet(),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
SizedBox(height: 24.h),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildListTile({
|
||||
required String icon,
|
||||
required String title,
|
||||
@@ -202,4 +470,4 @@ class ProfilePage extends StatelessWidget {
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user