added userdetails api get and put and more changes

This commit is contained in:
mystery012728
2026-01-28 19:28:37 +05:30
parent 1cb344738e
commit 0434b16bde
24 changed files with 1533 additions and 448 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

BIN
assets/images/not_login.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

View File

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

View File

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

View File

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

View File

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

View File

@@ -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'] ?? {},

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

@@ -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()));

View File

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

View File

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

View File

@@ -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 (_) {

View 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());
}
}

View 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();
}

View 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];
}

View 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,
};
}
}

View 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);
}
}

View File

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