Compare commits
7 Commits
b08e2699e9
...
shreeyash
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cdfb9c74ca | ||
| 0abdd2b796 | |||
| dd1991da09 | |||
|
|
48fd7037ea | ||
|
|
40f0ed3a52 | ||
|
|
53264619a8 | ||
|
|
68c3f28d76 |
Binary file not shown.
|
Before Width: | Height: | Size: 749 KiB After Width: | Height: | Size: 1.2 MiB |
210
lib/common_packages/custom_snackbar.dart
Normal file
210
lib/common_packages/custom_snackbar.dart
Normal file
@@ -0,0 +1,210 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
|
||||
class CustomSnackbar {
|
||||
static void show(
|
||||
BuildContext context, {
|
||||
required String message,
|
||||
Color? backgroundColor,
|
||||
Color? textColor,
|
||||
IconData? icon,
|
||||
Duration duration = const Duration(seconds: 3),
|
||||
bool useOverlay = false,
|
||||
}) {
|
||||
if (useOverlay) {
|
||||
_showOverlaySnackbar(
|
||||
context,
|
||||
message: message,
|
||||
backgroundColor: backgroundColor ?? Colors.black87,
|
||||
textColor: textColor ?? Colors.white,
|
||||
icon: icon,
|
||||
duration: duration,
|
||||
);
|
||||
} else {
|
||||
_showRegularSnackbar(
|
||||
context,
|
||||
message: message,
|
||||
backgroundColor: backgroundColor ?? Colors.black87,
|
||||
textColor: textColor ?? Colors.white,
|
||||
icon: icon,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
static void _showRegularSnackbar(
|
||||
BuildContext context, {
|
||||
required String message,
|
||||
required Color backgroundColor,
|
||||
required Color textColor,
|
||||
IconData? icon,
|
||||
}) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Row(
|
||||
children: [
|
||||
if (icon != null) ...[
|
||||
Icon(
|
||||
icon,
|
||||
color: textColor,
|
||||
size: 20.sp,
|
||||
),
|
||||
SizedBox(width: 12.w),
|
||||
],
|
||||
Expanded(
|
||||
child: Text(
|
||||
message,
|
||||
style: TextStyle(
|
||||
color: textColor,
|
||||
fontSize: 14.sp,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
backgroundColor: backgroundColor,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8.r),
|
||||
),
|
||||
margin: EdgeInsets.symmetric(horizontal: 16.w, vertical: 12.h),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static void _showOverlaySnackbar(
|
||||
BuildContext context, {
|
||||
required String message,
|
||||
required Color backgroundColor,
|
||||
required Color textColor,
|
||||
IconData? icon,
|
||||
required Duration duration,
|
||||
}) {
|
||||
final overlay = Overlay.of(context);
|
||||
final overlayEntry = OverlayEntry(
|
||||
builder: (context) => Positioned(
|
||||
top: MediaQuery.of(context).padding.top + 10,
|
||||
left: 20.w,
|
||||
right: 20.w,
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: TweenAnimationBuilder<double>(
|
||||
tween: Tween(begin: 0.0, end: 1.0),
|
||||
duration: const Duration(milliseconds: 300),
|
||||
builder: (context, value, child) {
|
||||
return Transform.translate(
|
||||
offset: Offset(0, -20 * (1 - value)),
|
||||
child: Opacity(
|
||||
opacity: value,
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 12.h),
|
||||
decoration: BoxDecoration(
|
||||
color: backgroundColor,
|
||||
borderRadius: BorderRadius.circular(8.r),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.3),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
if (icon != null) ...[
|
||||
Icon(
|
||||
icon,
|
||||
color: textColor,
|
||||
size: 20.sp,
|
||||
),
|
||||
SizedBox(width: 12.w),
|
||||
],
|
||||
Expanded(
|
||||
child: Text(
|
||||
message,
|
||||
style: TextStyle(
|
||||
color: textColor,
|
||||
fontSize: 14.sp,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
overlay.insert(overlayEntry);
|
||||
Future.delayed(duration, () {
|
||||
overlayEntry.remove();
|
||||
});
|
||||
}
|
||||
|
||||
// Helper methods for common use cases
|
||||
static void showSuccess(
|
||||
BuildContext context, {
|
||||
required String message,
|
||||
bool useOverlay = false,
|
||||
}) {
|
||||
show(
|
||||
context,
|
||||
message: message,
|
||||
backgroundColor: Colors.green,
|
||||
textColor: Colors.white,
|
||||
icon: Icons.check_circle,
|
||||
useOverlay: useOverlay,
|
||||
);
|
||||
}
|
||||
|
||||
static void showError(
|
||||
BuildContext context, {
|
||||
required String message,
|
||||
bool useOverlay = false,
|
||||
}) {
|
||||
show(
|
||||
context,
|
||||
message: message,
|
||||
backgroundColor: Colors.red,
|
||||
textColor: Colors.white,
|
||||
icon: Icons.error,
|
||||
useOverlay: useOverlay,
|
||||
);
|
||||
}
|
||||
|
||||
static void showWarning(
|
||||
BuildContext context, {
|
||||
required String message,
|
||||
bool useOverlay = false,
|
||||
}) {
|
||||
show(
|
||||
context,
|
||||
message: message,
|
||||
backgroundColor: Colors.orange,
|
||||
textColor: Colors.white,
|
||||
icon: Icons.warning,
|
||||
useOverlay: useOverlay,
|
||||
);
|
||||
}
|
||||
|
||||
static void showInfo(
|
||||
BuildContext context, {
|
||||
required String message,
|
||||
bool useOverlay = false,
|
||||
}) {
|
||||
show(
|
||||
context,
|
||||
message: message,
|
||||
backgroundColor: Colors.blue,
|
||||
textColor: Colors.white,
|
||||
icon: Icons.info,
|
||||
useOverlay: useOverlay,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:citycards_customer/common_packages/custom_text.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
class CustomTextField extends StatelessWidget {
|
||||
final String label;
|
||||
@@ -8,11 +9,15 @@ class CustomTextField extends StatelessWidget {
|
||||
final TextEditingController controller;
|
||||
final int? maxLines;
|
||||
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
|
||||
final String? Function(String?)? validator;
|
||||
final TextInputType? keyboardType;
|
||||
final bool obscureText;
|
||||
final Widget? suffixIcon;
|
||||
final void Function(String)? onChanged;
|
||||
|
||||
// ✅ NEW
|
||||
final int? maxLength; // e.g. 10
|
||||
final bool numbersOnly; // allow only digits
|
||||
|
||||
const CustomTextField({
|
||||
super.key,
|
||||
@@ -26,6 +31,10 @@ class CustomTextField extends StatelessWidget {
|
||||
this.obscureText = false,
|
||||
this.suffixIcon,
|
||||
this.onChanged,
|
||||
|
||||
// ✅ NEW
|
||||
this.maxLength, // default = null (infinite)
|
||||
this.numbersOnly = false, // default = false
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -42,16 +51,27 @@ class CustomTextField extends StatelessWidget {
|
||||
SizedBox(height: 6.h),
|
||||
SizedBox(
|
||||
height: maxLines == 1 ? 42.h : null,
|
||||
child: TextFormField( // ✅ Changed from TextField to TextFormField
|
||||
child: TextFormField(
|
||||
controller: controller,
|
||||
maxLines: obscureText ? 1 : maxLines, // ✅ Password fields always single line
|
||||
maxLines: obscureText ? 1 : maxLines,
|
||||
enabled: enabled,
|
||||
validator: validator, // ✅ Added validator
|
||||
keyboardType: keyboardType, // ✅ Added keyboard type
|
||||
obscureText: obscureText, // ✅ Added obscure text
|
||||
onChanged: onChanged, // ✅ Added onChanged
|
||||
validator: validator,
|
||||
keyboardType: keyboardType,
|
||||
obscureText: obscureText,
|
||||
onChanged: onChanged,
|
||||
|
||||
// ✅ NEW
|
||||
maxLength: maxLength,
|
||||
inputFormatters: [
|
||||
if (numbersOnly)
|
||||
FilteringTextInputFormatter.digitsOnly,
|
||||
if (maxLength != null)
|
||||
LengthLimitingTextInputFormatter(maxLength),
|
||||
],
|
||||
|
||||
decoration: InputDecoration(
|
||||
hintText: hint,
|
||||
counterText: "", // ✅ hides 0/10 counter
|
||||
hintStyle: TextStyle(
|
||||
fontSize: 12.sp,
|
||||
color: const Color(0xFF8E8E8E),
|
||||
@@ -62,9 +82,9 @@ class CustomTextField extends StatelessWidget {
|
||||
: Colors.grey.shade200,
|
||||
contentPadding: EdgeInsets.symmetric(
|
||||
horizontal: 24.w,
|
||||
vertical: maxLines != null && maxLines! > 1 ? 12.h : 0, // ✅ Better padding for multiline
|
||||
vertical: maxLines != null && maxLines! > 1 ? 12.h : 0,
|
||||
),
|
||||
suffixIcon: suffixIcon, // ✅ Added suffix icon
|
||||
suffixIcon: suffixIcon,
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8.r),
|
||||
borderSide: BorderSide(
|
||||
@@ -79,28 +99,21 @@ class CustomTextField extends StatelessWidget {
|
||||
width: 1.w,
|
||||
),
|
||||
),
|
||||
disabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8.r),
|
||||
borderSide: BorderSide(
|
||||
color: Colors.grey.shade400,
|
||||
width: .4.w,
|
||||
),
|
||||
),
|
||||
errorBorder: OutlineInputBorder( // ✅ NEW: Error state border
|
||||
errorBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8.r),
|
||||
borderSide: BorderSide(
|
||||
color: Colors.red,
|
||||
width: 1.w,
|
||||
),
|
||||
),
|
||||
focusedErrorBorder: OutlineInputBorder( // ✅ NEW: Focused error state
|
||||
focusedErrorBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8.r),
|
||||
borderSide: BorderSide(
|
||||
color: Colors.red,
|
||||
width: 1.5.w,
|
||||
),
|
||||
),
|
||||
errorStyle: TextStyle( // ✅ NEW: Error text style
|
||||
errorStyle: TextStyle(
|
||||
fontSize: 11.sp,
|
||||
color: Colors.red,
|
||||
),
|
||||
|
||||
@@ -186,7 +186,7 @@ class AppRouter {
|
||||
final attractionID = settings.arguments as int;
|
||||
return MaterialPageRoute(
|
||||
builder: (_) {
|
||||
return PassAttractionDetailsView(attractionId: attractionID);
|
||||
return AttractionDetailsView(attractionId: attractionID);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -201,9 +201,7 @@ class AppRouter {
|
||||
final bookingId = settings.arguments as int; // or String
|
||||
|
||||
return MaterialPageRoute(
|
||||
builder: (_) => CheckoutView(
|
||||
bookingId: bookingId,
|
||||
),
|
||||
builder: (_) => CheckoutView(bookingId: bookingId),
|
||||
);
|
||||
|
||||
|
||||
@@ -239,9 +237,7 @@ class AppRouter {
|
||||
|
||||
return MaterialPageRoute(
|
||||
builder: (_) {
|
||||
return AddDetailsView(
|
||||
bookingId: bookingId,
|
||||
);
|
||||
return AddDetailsView(bookingId: bookingId);
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
6
lib/core/global_keys.dart
Normal file
6
lib/core/global_keys.dart
Normal file
@@ -0,0 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class GlobalKeys {
|
||||
static final GlobalKey<ScaffoldMessengerState> scaffoldMessengerKey =
|
||||
GlobalKey<ScaffoldMessengerState>();
|
||||
}
|
||||
@@ -28,7 +28,8 @@ class RouteConstants {
|
||||
static const String magicItineraryEmptyScreen = '/magicItineraryEmptyScreen';
|
||||
static const String itineraryCreationStart = '/itineraryCreationStart';
|
||||
static const String itineraryCreation = '/itineraryCreation';
|
||||
static const String magicItineraryFilledScreen = "/magicItineraryFilledScreen";
|
||||
static const String magicItineraryFilledScreen =
|
||||
"/magicItineraryFilledScreen";
|
||||
|
||||
/**************************** ESIM Page *****************************************/
|
||||
|
||||
@@ -42,8 +43,8 @@ class RouteConstants {
|
||||
|
||||
/**************************** By Pass Page Page *****************************************/
|
||||
|
||||
static const String buyPass ='/buyPass';
|
||||
static const String checkout ='/checkout';
|
||||
static const String buyPass = '/buyPass';
|
||||
static const String checkout = '/checkout';
|
||||
static const String searchOffer = '/searchOffer';
|
||||
static const String searchPassOffer = '/searchPassOffer';
|
||||
static const String createAcct = '/createAcct';
|
||||
@@ -59,4 +60,5 @@ class RouteConstants {
|
||||
static const String qrPage = '/qrPage';
|
||||
static const String makeBooking = '/makeBooking';
|
||||
static const String bookingSuccessful = '/bookingSuccessful';
|
||||
static const String editPostCard = '/editPostCard';
|
||||
}
|
||||
|
||||
@@ -194,6 +194,8 @@ class _CreateAccountViewState extends State<CreateAccountView> {
|
||||
label: "Phone Number",
|
||||
hint: "Enter your phone number",
|
||||
controller: phoneController,
|
||||
keyboardType: TextInputType.number,
|
||||
maxLength: 10,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -358,6 +360,7 @@ class _CreateAccountViewState extends State<CreateAccountView> {
|
||||
hint: "Enter postal / zip code",
|
||||
controller: postalController,
|
||||
keyboardType: TextInputType.number,
|
||||
maxLength: 6,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 20.h),
|
||||
|
||||
@@ -23,30 +23,47 @@ class CurrentLocationSelection extends StatefulWidget {
|
||||
class _CurrentLocationSelectionState extends State<CurrentLocationSelection> {
|
||||
final TextEditingController _controller = TextEditingController();
|
||||
LatLng? _currentLatLng;
|
||||
bool loading = false;
|
||||
|
||||
Future<void> _getCurrentLocation() async {
|
||||
LocationPermission permission = await Geolocator.requestPermission();
|
||||
try {
|
||||
setState(() {
|
||||
loading = true;
|
||||
});
|
||||
LocationPermission permission = await Geolocator.requestPermission();
|
||||
|
||||
if (permission == LocationPermission.denied ||
|
||||
permission == LocationPermission.deniedForever) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Location permission denied')),
|
||||
if (permission == LocationPermission.denied ||
|
||||
permission == LocationPermission.deniedForever) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Location permission denied')),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final position = await Geolocator.getCurrentPosition(
|
||||
desiredAccuracy: LocationAccuracy.high,
|
||||
);
|
||||
return;
|
||||
|
||||
final lat = position.latitude;
|
||||
final lng = position.longitude;
|
||||
|
||||
setState(() {
|
||||
_currentLatLng = LatLng(lat, lng);
|
||||
});
|
||||
|
||||
await _getAddressFromLatLng(lat, lng);
|
||||
setState(() {
|
||||
loading = false;
|
||||
});
|
||||
} catch (e) {
|
||||
setState(() {
|
||||
loading = false;
|
||||
});
|
||||
} finally {
|
||||
setState(() {
|
||||
loading = false;
|
||||
});
|
||||
}
|
||||
|
||||
final position = await Geolocator.getCurrentPosition(
|
||||
desiredAccuracy: LocationAccuracy.high,
|
||||
);
|
||||
|
||||
final lat = position.latitude;
|
||||
final lng = position.longitude;
|
||||
|
||||
setState(() {
|
||||
_currentLatLng = LatLng(lat, lng);
|
||||
});
|
||||
|
||||
await _getAddressFromLatLng(lat, lng);
|
||||
}
|
||||
|
||||
Future<void> _getAddressFromLatLng(double lat, double lng) async {
|
||||
@@ -57,7 +74,6 @@ class _CurrentLocationSelectionState extends State<CurrentLocationSelection> {
|
||||
final place = placemarks.first;
|
||||
|
||||
final address = [
|
||||
place.name,
|
||||
place.street,
|
||||
place.subLocality,
|
||||
place.locality,
|
||||
@@ -133,34 +149,41 @@ class _CurrentLocationSelectionState extends State<CurrentLocationSelection> {
|
||||
child: SizedBox(
|
||||
height: 250.h,
|
||||
width: double.infinity,
|
||||
child: FlutterMap(
|
||||
options: MapOptions(
|
||||
initialCenter: _currentLatLng!,
|
||||
initialZoom: 15,
|
||||
),
|
||||
children: [
|
||||
TileLayer(
|
||||
urlTemplate:
|
||||
"https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",
|
||||
subdomains: const ['a', 'b', 'c'],
|
||||
userAgentPackageName: 'com.citycards.customer',
|
||||
),
|
||||
MarkerLayer(
|
||||
markers: [
|
||||
Marker(
|
||||
point: _currentLatLng!,
|
||||
width: 40,
|
||||
height: 40,
|
||||
child: const Icon(
|
||||
Icons.location_pin,
|
||||
color: Colors.red,
|
||||
size: 40,
|
||||
),
|
||||
child: loading == true
|
||||
? Center(
|
||||
child: CircularProgressIndicator(
|
||||
color: Color(0xFFF95F62),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
: FlutterMap(
|
||||
options: MapOptions(
|
||||
initialCenter: _currentLatLng!,
|
||||
initialZoom: 15,
|
||||
),
|
||||
children: [
|
||||
TileLayer(
|
||||
urlTemplate:
|
||||
"https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",
|
||||
subdomains: const ['a', 'b', 'c'],
|
||||
userAgentPackageName:
|
||||
'com.citycards.customer',
|
||||
),
|
||||
MarkerLayer(
|
||||
markers: [
|
||||
Marker(
|
||||
point: _currentLatLng!,
|
||||
width: 40,
|
||||
height: 40,
|
||||
child: const Icon(
|
||||
Icons.location_pin,
|
||||
color: Colors.red,
|
||||
size: 40,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
: GestureDetector(
|
||||
|
||||
@@ -5,6 +5,7 @@ import 'package:citycards_customer/login/view/verify_otp_bottomsheet.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import '../../common_packages/custom_snackbar.dart';
|
||||
import '../bloc/login/login_bloc.dart';
|
||||
import '../bloc/login/login_state.dart';
|
||||
import '../bloc/login/login_event.dart';
|
||||
@@ -52,11 +53,10 @@ class _LoginEmailBottomsheetState extends State<LoginEmailBottomsheet> {
|
||||
),
|
||||
);
|
||||
} else if (state is LoginError) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(state.errorMessage),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
CustomSnackbar.showError(
|
||||
context,
|
||||
message: state.errorMessage,
|
||||
useOverlay: true, // Use overlay to show above bottom sheet
|
||||
);
|
||||
}
|
||||
},
|
||||
@@ -116,11 +116,10 @@ class _LoginEmailBottomsheetState extends State<LoginEmailBottomsheet> {
|
||||
|
||||
final email = _emailController.text.trim();
|
||||
if (email.isEmpty) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Please enter your email'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
CustomSnackbar.showError(
|
||||
context,
|
||||
message: "Please enter your email",
|
||||
useOverlay: true, // Use overlay to show above bottom sheet
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_otp_text_field/flutter_otp_text_field.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import '../../common_packages/custom_snackbar.dart';
|
||||
import '../../core/route_constants.dart';
|
||||
import '../../localPreference/local_preference.dart';
|
||||
import '../../postcard/blocs/myPostCards/my_postcard_event.dart';
|
||||
@@ -77,11 +78,10 @@ class _VerifyOtpBottomsheetState extends State<VerifyOtpBottomsheet> {
|
||||
),
|
||||
);
|
||||
} else if (state is VerifyOtpError) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(state.errorMessage),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
CustomSnackbar.showError(
|
||||
context,
|
||||
message: state.errorMessage,
|
||||
useOverlay: true, // Use overlay to show above bottom sheet
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -11,6 +11,7 @@ import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:flutter_stripe/flutter_stripe.dart'; // ADD THIS
|
||||
import 'cart/blocs/myPassCart/my_pass_cart_bloc.dart';
|
||||
import 'core/app_router.dart';
|
||||
import 'core/global_keys.dart';
|
||||
import 'home/bloc/FirstTimeUserHome/first_time_user_home_bloc.dart';
|
||||
import 'home/bloc/FirstTimeUserHome/first_time_user_home_event.dart';
|
||||
import 'home/bloc/registeredHome/home_bloc.dart';
|
||||
@@ -99,6 +100,7 @@ class MyApp extends StatelessWidget {
|
||||
)
|
||||
],
|
||||
child: MaterialApp(
|
||||
scaffoldMessengerKey: GlobalKeys.scaffoldMessengerKey,
|
||||
onGenerateRoute: _appRouter.onGenerateRoute,
|
||||
initialRoute: RouteConstants.splash,
|
||||
debugShowCheckedModeBanner: false,
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
class ApiUrls {
|
||||
|
||||
// static const baseUrl = "https://devapi.citycards.betadelivery.com";//Normal API
|
||||
static const baseUrl = "https://testingapi.citycards.betadelivery.com";// Test API
|
||||
static const baseUrl =
|
||||
"https://devapi.citycards.betadelivery.com"; //Normal API
|
||||
//static const baseUrl =
|
||||
// "https://testingapi.citycards.betadelivery.com"; // Test API
|
||||
// static const baseUrl = "https://uatapi.citycard.betadelivery.com";// Production Lvl API
|
||||
|
||||
static const refreshToken = "$baseUrl/auth/refresh";
|
||||
@@ -25,8 +26,11 @@ class ApiUrls {
|
||||
static const passDetails = "$baseUrl/mobile/passes";
|
||||
static const myPassesCart = "$baseUrl/mobile/passes/cart/passes";
|
||||
|
||||
static const editPostcard = "$baseUrl/mobile/postcards";
|
||||
|
||||
static const myItineraries = "$baseUrl/mobile/itinerary/all-initineraries";
|
||||
static const getItineraryCities = "$baseUrl/mobile/itinerary/cities-with-icons";
|
||||
static const getItineraryCities =
|
||||
"$baseUrl/mobile/itinerary/cities-with-icons";
|
||||
|
||||
//Post Apis
|
||||
static const createAccount = "$baseUrl/mobile/user/register";
|
||||
|
||||
@@ -185,6 +185,27 @@ class NetworkApiService {
|
||||
}
|
||||
}
|
||||
|
||||
// ================= DELETE =================
|
||||
Future<Response> deleteApi({
|
||||
required String url,
|
||||
dynamic data,
|
||||
Map<String, dynamic>? queryParameters,
|
||||
Options? options,
|
||||
CancelToken? cancelToken,
|
||||
}) async {
|
||||
try {
|
||||
return await _dio.delete(
|
||||
url,
|
||||
data: data,
|
||||
queryParameters: queryParameters,
|
||||
options: options,
|
||||
cancelToken: cancelToken,
|
||||
);
|
||||
} on DioException catch (e) {
|
||||
throw _handleError(e);
|
||||
}
|
||||
}
|
||||
|
||||
// ================= REFRESH TOKEN =================
|
||||
Future<bool> _refreshToken() async {
|
||||
try {
|
||||
@@ -225,7 +246,7 @@ class NetworkApiService {
|
||||
case DioExceptionType.badCertificate:
|
||||
return "Bad certificate.";
|
||||
case DioExceptionType.badResponse:
|
||||
// 🔥 FIXED: Safely handle different response data types
|
||||
// 🔥 FIXED: Safely handle different response data types
|
||||
try {
|
||||
final responseData = error.response?.data;
|
||||
|
||||
|
||||
150
lib/postcard/blocs/edit_image_filter/edit_image_filter_bloc.dart
Normal file
150
lib/postcard/blocs/edit_image_filter/edit_image_filter_bloc.dart
Normal file
@@ -0,0 +1,150 @@
|
||||
import 'dart:developer';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:image/image.dart' as img;
|
||||
|
||||
part 'edit_image_filter_event.dart';
|
||||
part 'edit_image_filter_state.dart';
|
||||
|
||||
enum EditImageType { network, file }
|
||||
|
||||
class EditImageFilterBloc
|
||||
extends Bloc<EditImageFilterEvent, EditImageFilterState> {
|
||||
EditImageFilterBloc() : super(EditImageFilterInitial()) {
|
||||
on<DownloadImage>((event, emit) async {
|
||||
try {
|
||||
emit(DownloadImageLoading());
|
||||
if (event.type == EditImageType.network) {
|
||||
final Dio dio = Dio();
|
||||
|
||||
final directory = await getTemporaryDirectory();
|
||||
|
||||
final String fileName =
|
||||
'${DateTime.now().millisecondsSinceEpoch}.png';
|
||||
|
||||
final String filePath = '${directory.path}/$fileName';
|
||||
|
||||
await dio.download(
|
||||
event.url,
|
||||
filePath,
|
||||
options: Options(responseType: ResponseType.bytes),
|
||||
);
|
||||
|
||||
emit(
|
||||
DownloadImageSuccessfully(
|
||||
filePath: filePath,
|
||||
filteredImagePath: filePath,
|
||||
filter: 'original',
|
||||
),
|
||||
);
|
||||
} else {
|
||||
emit(
|
||||
DownloadImageSuccessfully(
|
||||
filePath: event.url,
|
||||
filteredImagePath: event.url,
|
||||
filter: 'original',
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
emit(DownloadImageFailed());
|
||||
}
|
||||
});
|
||||
on<SelectFilter>((event, emit) async {
|
||||
if (state is! DownloadImageSuccessfully) return;
|
||||
|
||||
final currentState = state as DownloadImageSuccessfully;
|
||||
|
||||
try {
|
||||
log("Selected Filter ${event.filterName}");
|
||||
emit(currentState.copyWith(processing: true));
|
||||
|
||||
if (event.filterName == "none" || event.filterName == "original") {
|
||||
emit(
|
||||
currentState.copyWith(
|
||||
filteredImagePath: currentState.filePath,
|
||||
processing: false,
|
||||
filter: "original",
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final originalFile = File(currentState.filePath);
|
||||
final bytes = await originalFile.readAsBytes();
|
||||
img.Image? image = img.decodeImage(bytes);
|
||||
|
||||
if (image == null) {
|
||||
emit(currentState.copyWith(processing: false));
|
||||
return;
|
||||
}
|
||||
|
||||
switch (event.filterName) {
|
||||
case "vintage":
|
||||
image = img.adjustColor(
|
||||
image,
|
||||
saturation: 0.8,
|
||||
gamma: 1.1,
|
||||
contrast: 0.9,
|
||||
);
|
||||
break;
|
||||
case "bw":
|
||||
image = img.grayscale(image);
|
||||
break;
|
||||
case "sepia":
|
||||
image = img.sepia(image);
|
||||
break;
|
||||
case "cool":
|
||||
// hue is normalized 0.0–1.0; -15 degrees ≈ -15/360 ≈ -0.042
|
||||
image = img.adjustColor(image, hue: -0.042, contrast: 1.05);
|
||||
break;
|
||||
case "contrast":
|
||||
image = img.adjustColor(image, contrast: 1.4);
|
||||
break;
|
||||
case "soft":
|
||||
image = img.adjustColor(
|
||||
image,
|
||||
brightness: 0.1,
|
||||
gamma: 0.9,
|
||||
saturation: 1.1,
|
||||
);
|
||||
break;
|
||||
default:
|
||||
emit(currentState.copyWith(filter: "none", processing: false));
|
||||
return;
|
||||
}
|
||||
|
||||
final filteredPath =
|
||||
"${originalFile.parent.path}/filtered_${event.filterName}_${DateTime.now().millisecondsSinceEpoch}.jpg";
|
||||
|
||||
final filteredFile = File(filteredPath)
|
||||
..writeAsBytesSync(img.encodeJpg(image, quality: 95));
|
||||
|
||||
if (currentState.filteredImagePath != currentState.filePath) {
|
||||
final oldFile = File(currentState.filteredImagePath);
|
||||
if (await oldFile.exists()) await oldFile.delete();
|
||||
}
|
||||
|
||||
log(
|
||||
"Filter applied: ${filteredFile.path} | filter: ${event.filterName}",
|
||||
);
|
||||
|
||||
emit(
|
||||
currentState.copyWith(
|
||||
filteredImagePath: filteredFile.path,
|
||||
filter: event.filterName,
|
||||
processing: false,
|
||||
),
|
||||
);
|
||||
return;
|
||||
} catch (e) {
|
||||
log("SelectFilter error: ${e.toString()}");
|
||||
emit(currentState.copyWith(processing: false)); // don't leave UI stuck
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
part of 'edit_image_filter_bloc.dart';
|
||||
|
||||
class EditImageFilterEvent extends Equatable {
|
||||
const EditImageFilterEvent();
|
||||
|
||||
@override
|
||||
List<Object> get props => [];
|
||||
}
|
||||
|
||||
class DownloadImage extends EditImageFilterEvent {
|
||||
final String url;
|
||||
final EditImageType type;
|
||||
const DownloadImage({required this.url, required this.type});
|
||||
}
|
||||
|
||||
class SelectFilter extends EditImageFilterEvent {
|
||||
final String filterName;
|
||||
const SelectFilter({required this.filterName});
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
part of 'edit_image_filter_bloc.dart';
|
||||
|
||||
class EditImageFilterState extends Equatable {
|
||||
const EditImageFilterState();
|
||||
|
||||
@override
|
||||
List<Object> get props => [];
|
||||
}
|
||||
|
||||
class EditImageFilterInitial extends EditImageFilterState {}
|
||||
|
||||
class DownloadImageLoading extends EditImageFilterState {}
|
||||
|
||||
class DownloadImageSuccessfully extends EditImageFilterState {
|
||||
final String filePath;
|
||||
final String filteredImagePath;
|
||||
final bool processing;
|
||||
final String filter;
|
||||
|
||||
const DownloadImageSuccessfully({
|
||||
required this.filePath,
|
||||
required this.filteredImagePath,
|
||||
this.processing = false,
|
||||
required this.filter,
|
||||
});
|
||||
@override
|
||||
List<Object> get props => [filePath, filteredImagePath, processing, filter];
|
||||
|
||||
DownloadImageSuccessfully copyWith({
|
||||
String? filePath,
|
||||
String? filteredImagePath,
|
||||
bool? processing,
|
||||
String? filter,
|
||||
}) {
|
||||
return DownloadImageSuccessfully(
|
||||
filePath: filePath ?? this.filePath,
|
||||
filteredImagePath: filteredImagePath ?? this.filteredImagePath,
|
||||
processing: processing ?? this.processing,
|
||||
filter: filter ?? this.filter,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class DownloadImageFailed extends EditImageFilterState {}
|
||||
27
lib/postcard/blocs/edit_postcard/edit_postcard_bloc.dart
Normal file
27
lib/postcard/blocs/edit_postcard/edit_postcard_bloc.dart
Normal file
@@ -0,0 +1,27 @@
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:citycards_customer/postcard/models/my_postcard_model.dart';
|
||||
import 'package:citycards_customer/postcard/repository/my_postcard_repository.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
part 'edit_postcard_event.dart';
|
||||
part 'edit_postcard_state.dart';
|
||||
|
||||
class EditPostcardBloc extends Bloc<EditPostcardEvent, EditPostcardState> {
|
||||
EditPostcardBloc() : super(EditPostcardInitial()) {
|
||||
on<EditPostCard>((event, emit) async {
|
||||
try {
|
||||
emit(EditPostcardLoading());
|
||||
await MyPostCardsRepository().editMyPostCards(
|
||||
postcard: event.myPostCard,
|
||||
image: event.editImage,
|
||||
);
|
||||
log("Edit PostCard Successfully");
|
||||
emit(EditPostcardSuccessfull());
|
||||
} catch (e) {
|
||||
emit(EditPostcardError(error: "Failed to edit postcard"));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
14
lib/postcard/blocs/edit_postcard/edit_postcard_event.dart
Normal file
14
lib/postcard/blocs/edit_postcard/edit_postcard_event.dart
Normal file
@@ -0,0 +1,14 @@
|
||||
part of 'edit_postcard_bloc.dart';
|
||||
|
||||
class EditPostcardEvent extends Equatable {
|
||||
const EditPostcardEvent();
|
||||
|
||||
@override
|
||||
List<Object> get props => [];
|
||||
}
|
||||
|
||||
class EditPostCard extends EditPostcardEvent {
|
||||
final MyPostCard myPostCard;
|
||||
final String? editImage;
|
||||
const EditPostCard({required this.myPostCard, this.editImage});
|
||||
}
|
||||
19
lib/postcard/blocs/edit_postcard/edit_postcard_state.dart
Normal file
19
lib/postcard/blocs/edit_postcard/edit_postcard_state.dart
Normal file
@@ -0,0 +1,19 @@
|
||||
part of 'edit_postcard_bloc.dart';
|
||||
|
||||
class EditPostcardState extends Equatable {
|
||||
const EditPostcardState();
|
||||
|
||||
@override
|
||||
List<Object> get props => [];
|
||||
}
|
||||
|
||||
class EditPostcardInitial extends EditPostcardState {}
|
||||
|
||||
class EditPostcardLoading extends EditPostcardState {}
|
||||
|
||||
class EditPostcardSuccessfull extends EditPostcardState {}
|
||||
|
||||
class EditPostcardError extends EditPostcardState {
|
||||
final String error;
|
||||
const EditPostcardError({required this.error});
|
||||
}
|
||||
@@ -1,6 +1,9 @@
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:citycards_customer/localPreference/local_preference.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'dart:developer' as developer;
|
||||
import '../../models/my_postcard_model.dart';
|
||||
import '../../repository/my_postcard_repository.dart';
|
||||
import 'my_postcard_event.dart';
|
||||
import 'my_postcard_state.dart';
|
||||
@@ -8,12 +11,18 @@ import 'my_postcard_state.dart';
|
||||
class MyPostCardBloc extends Bloc<MyPostCardEvent, MyPostCardState> {
|
||||
final MyPostCardsRepository repository;
|
||||
|
||||
MyPostCardBloc({required this.repository}) : super(const MyPostCardInitial()) {
|
||||
MyPostCardBloc({required this.repository})
|
||||
: super(const MyPostCardInitial()) {
|
||||
on<CheckLoginStatus>(_onCheckLoginStatus);
|
||||
on<FetchDraftPostCards>(_onFetchDraftPostCards);
|
||||
on<FetchOrderPostCards>(_onFetchOrderPostCards);
|
||||
on<RefreshDraftPostCards>(_onRefreshDraftPostCards);
|
||||
on<RefreshOrderPostCards>(_onRefreshOrderPostCards);
|
||||
on<DeleteDraftPostCards>(_onDeletePostCard);
|
||||
on<SearchDraftPostCards>(_onSearchDraftPostCards);
|
||||
on<SearchOrderPostCards>(_onSearchOrderPostCards);
|
||||
on<ClearDraftSearch>(_onClearDraftSearch);
|
||||
on<ClearOrderSearch>(_onClearOrderSearch);
|
||||
}
|
||||
|
||||
/// Handle checking login status
|
||||
@@ -29,20 +38,28 @@ class MyPostCardBloc extends Bloc<MyPostCardEvent, MyPostCardState> {
|
||||
developer.log('📊 Login status: $isLogin', name: 'MyPostCardBloc');
|
||||
|
||||
if (isLogin) {
|
||||
developer.log('✅ User is logged in - initializing state', name: 'MyPostCardBloc');
|
||||
developer.log(
|
||||
'✅ User is logged in - initializing state',
|
||||
name: 'MyPostCardBloc',
|
||||
);
|
||||
// User is logged in, initialize with empty lists and loading states
|
||||
emit(const MyPostCardLoaded(
|
||||
draftPostCards: [],
|
||||
orderPostCards: [],
|
||||
isDraftLoading: true,
|
||||
isOrderLoading: true,
|
||||
));
|
||||
emit(
|
||||
const MyPostCardLoaded(
|
||||
draftPostCards: [],
|
||||
orderPostCards: [],
|
||||
isDraftLoading: true,
|
||||
isOrderLoading: true,
|
||||
),
|
||||
);
|
||||
|
||||
// Fetch both drafts and orders
|
||||
add(const FetchDraftPostCards());
|
||||
add(const FetchOrderPostCards());
|
||||
} else {
|
||||
developer.log('❌ User is NOT logged in - emitting MyPostCardNotLoggedIn', name: 'MyPostCardBloc');
|
||||
developer.log(
|
||||
'❌ User is NOT logged in - emitting MyPostCardNotLoggedIn',
|
||||
name: 'MyPostCardBloc',
|
||||
);
|
||||
// User is not logged in
|
||||
emit(const MyPostCardNotLoggedIn());
|
||||
}
|
||||
@@ -69,23 +86,35 @@ class MyPostCardBloc extends Bloc<MyPostCardEvent, MyPostCardState> {
|
||||
|
||||
try {
|
||||
final draftPostCards = await repository.fetchMyPostCards(type: 'draft');
|
||||
developer.log('✅ Draft postcards fetched: ${draftPostCards.length} items', name: 'MyPostCardBloc');
|
||||
developer.log(
|
||||
'✅ Draft postcards fetched: ${draftPostCards.length} items',
|
||||
name: 'MyPostCardBloc',
|
||||
);
|
||||
|
||||
if (state is MyPostCardLoaded) {
|
||||
// Update with fetched drafts
|
||||
emit((state as MyPostCardLoaded).copyWith(
|
||||
draftPostCards: draftPostCards,
|
||||
isDraftLoading: false,
|
||||
));
|
||||
// Update with fetched drafts and store in allDraftPostCards
|
||||
emit(
|
||||
(state as MyPostCardLoaded).copyWith(
|
||||
draftPostCards: draftPostCards,
|
||||
allDraftPostCards: draftPostCards, // Store original list
|
||||
isDraftLoading: false,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
developer.log('⚠️ State is not MyPostCardLoaded, creating new state', name: 'MyPostCardBloc');
|
||||
developer.log(
|
||||
'⚠️ State is not MyPostCardLoaded, creating new state',
|
||||
name: 'MyPostCardBloc',
|
||||
);
|
||||
// Fallback: create new loaded state (shouldn't normally happen)
|
||||
emit(MyPostCardLoaded(
|
||||
draftPostCards: draftPostCards,
|
||||
orderPostCards: const [],
|
||||
isDraftLoading: false,
|
||||
isOrderLoading: false,
|
||||
));
|
||||
emit(
|
||||
MyPostCardLoaded(
|
||||
draftPostCards: draftPostCards,
|
||||
orderPostCards: const [],
|
||||
allDraftPostCards: draftPostCards, // Store original list
|
||||
isDraftLoading: false,
|
||||
isOrderLoading: false,
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
developer.log('❌ Error fetching drafts: $error', name: 'MyPostCardBloc');
|
||||
@@ -95,10 +124,38 @@ class MyPostCardBloc extends Bloc<MyPostCardEvent, MyPostCardState> {
|
||||
}
|
||||
|
||||
// Emit error state
|
||||
emit(MyPostCardError(
|
||||
errorMessage: error.toString(),
|
||||
errorType: 'draft',
|
||||
));
|
||||
emit(MyPostCardError(errorMessage: error.toString(), errorType: 'draft'));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onDeletePostCard(
|
||||
DeleteDraftPostCards event,
|
||||
Emitter<MyPostCardState> emit,
|
||||
) async {
|
||||
if (state is MyPostCardLoaded) {
|
||||
MyPostCardLoaded currentState = state as MyPostCardLoaded;
|
||||
try {
|
||||
emit(currentState.copyWith(isDeleteLoading: true));
|
||||
await MyPostCardsRepository().deleteMyPostCards(event.id);
|
||||
|
||||
// Remove from both filtered and all lists
|
||||
List<MyPostCard> filteredItems = List.from(currentState.draftPostCards);
|
||||
List<MyPostCard> allItems = List.from(currentState.allDraftPostCards);
|
||||
|
||||
filteredItems.removeWhere((e) => e.id == event.id);
|
||||
allItems.removeWhere((e) => e.id == event.id);
|
||||
|
||||
emit(
|
||||
currentState.copyWith(
|
||||
draftPostCards: filteredItems,
|
||||
allDraftPostCards: allItems,
|
||||
isDeleteLoading: false,
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
log("Erro - $e");
|
||||
emit(currentState.copyWith(isDeleteLoading: false));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,23 +175,35 @@ class MyPostCardBloc extends Bloc<MyPostCardEvent, MyPostCardState> {
|
||||
|
||||
try {
|
||||
final orderPostCards = await repository.fetchMyPostCards(type: 'orders');
|
||||
developer.log('✅ Order postcards fetched: ${orderPostCards.length} items', name: 'MyPostCardBloc');
|
||||
developer.log(
|
||||
'✅ Order postcards fetched: ${orderPostCards.length} items',
|
||||
name: 'MyPostCardBloc',
|
||||
);
|
||||
|
||||
if (state is MyPostCardLoaded) {
|
||||
// Update with fetched orders
|
||||
emit((state as MyPostCardLoaded).copyWith(
|
||||
orderPostCards: orderPostCards,
|
||||
isOrderLoading: false,
|
||||
));
|
||||
// Update with fetched orders and store in allOrderPostCards
|
||||
emit(
|
||||
(state as MyPostCardLoaded).copyWith(
|
||||
orderPostCards: orderPostCards,
|
||||
allOrderPostCards: orderPostCards, // Store original list
|
||||
isOrderLoading: false,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
developer.log('⚠️ State is not MyPostCardLoaded, creating new state', name: 'MyPostCardBloc');
|
||||
developer.log(
|
||||
'⚠️ State is not MyPostCardLoaded, creating new state',
|
||||
name: 'MyPostCardBloc',
|
||||
);
|
||||
// Fallback: create new loaded state (shouldn't normally happen)
|
||||
emit(MyPostCardLoaded(
|
||||
draftPostCards: const [],
|
||||
orderPostCards: orderPostCards,
|
||||
isDraftLoading: false,
|
||||
isOrderLoading: false,
|
||||
));
|
||||
emit(
|
||||
MyPostCardLoaded(
|
||||
draftPostCards: const [],
|
||||
orderPostCards: orderPostCards,
|
||||
allOrderPostCards: orderPostCards, // Store original list
|
||||
isDraftLoading: false,
|
||||
isOrderLoading: false,
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
developer.log('❌ Error fetching orders: $error', name: 'MyPostCardBloc');
|
||||
@@ -144,10 +213,7 @@ class MyPostCardBloc extends Bloc<MyPostCardEvent, MyPostCardState> {
|
||||
}
|
||||
|
||||
// Emit error state
|
||||
emit(MyPostCardError(
|
||||
errorMessage: error.toString(),
|
||||
errorType: 'order',
|
||||
));
|
||||
emit(MyPostCardError(errorMessage: error.toString(), errorType: 'order'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -159,19 +225,40 @@ class MyPostCardBloc extends Bloc<MyPostCardEvent, MyPostCardState> {
|
||||
developer.log('🔄 Refreshing draft postcards...', name: 'MyPostCardBloc');
|
||||
try {
|
||||
final draftPostCards = await repository.fetchMyPostCards(type: 'draft');
|
||||
developer.log('✅ Draft postcards refreshed: ${draftPostCards.length} items', name: 'MyPostCardBloc');
|
||||
developer.log(
|
||||
'✅ Draft postcards refreshed: ${draftPostCards.length} items',
|
||||
name: 'MyPostCardBloc',
|
||||
);
|
||||
|
||||
if (state is MyPostCardLoaded) {
|
||||
emit((state as MyPostCardLoaded).copyWith(
|
||||
draftPostCards: draftPostCards,
|
||||
));
|
||||
final currentState = state as MyPostCardLoaded;
|
||||
// If there's an active search, apply it to the new data
|
||||
if (currentState.draftSearchQuery.isNotEmpty) {
|
||||
final filteredDrafts = _filterPostCards(
|
||||
draftPostCards,
|
||||
currentState.draftSearchQuery,
|
||||
);
|
||||
emit(
|
||||
currentState.copyWith(
|
||||
draftPostCards: filteredDrafts,
|
||||
allDraftPostCards: draftPostCards,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
emit(
|
||||
currentState.copyWith(
|
||||
draftPostCards: draftPostCards,
|
||||
allDraftPostCards: draftPostCards,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
developer.log('❌ Error refreshing drafts: $error', name: 'MyPostCardBloc');
|
||||
emit(MyPostCardError(
|
||||
errorMessage: error.toString(),
|
||||
errorType: 'draft',
|
||||
));
|
||||
developer.log(
|
||||
'❌ Error refreshing drafts: $error',
|
||||
name: 'MyPostCardBloc',
|
||||
);
|
||||
emit(MyPostCardError(errorMessage: error.toString(), errorType: 'draft'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -183,19 +270,189 @@ class MyPostCardBloc extends Bloc<MyPostCardEvent, MyPostCardState> {
|
||||
developer.log('🔄 Refreshing order postcards...', name: 'MyPostCardBloc');
|
||||
try {
|
||||
final orderPostCards = await repository.fetchMyPostCards(type: 'orders');
|
||||
developer.log('✅ Order postcards refreshed: ${orderPostCards.length} items', name: 'MyPostCardBloc');
|
||||
developer.log(
|
||||
'✅ Order postcards refreshed: ${orderPostCards.length} items',
|
||||
name: 'MyPostCardBloc',
|
||||
);
|
||||
|
||||
if (state is MyPostCardLoaded) {
|
||||
emit((state as MyPostCardLoaded).copyWith(
|
||||
orderPostCards: orderPostCards,
|
||||
));
|
||||
final currentState = state as MyPostCardLoaded;
|
||||
// If there's an active search, apply it to the new data
|
||||
if (currentState.orderSearchQuery.isNotEmpty) {
|
||||
final filteredOrders = _filterPostCards(
|
||||
orderPostCards,
|
||||
currentState.orderSearchQuery,
|
||||
);
|
||||
emit(
|
||||
currentState.copyWith(
|
||||
orderPostCards: filteredOrders,
|
||||
allOrderPostCards: orderPostCards,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
emit(
|
||||
currentState.copyWith(
|
||||
orderPostCards: orderPostCards,
|
||||
allOrderPostCards: orderPostCards,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
developer.log('❌ Error refreshing orders: $error', name: 'MyPostCardBloc');
|
||||
emit(MyPostCardError(
|
||||
errorMessage: error.toString(),
|
||||
errorType: 'order',
|
||||
));
|
||||
developer.log(
|
||||
'❌ Error refreshing orders: $error',
|
||||
name: 'MyPostCardBloc',
|
||||
);
|
||||
emit(MyPostCardError(errorMessage: error.toString(), errorType: 'order'));
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle searching draft postcards
|
||||
Future<void> _onSearchDraftPostCards(
|
||||
SearchDraftPostCards event,
|
||||
Emitter<MyPostCardState> emit,
|
||||
) async {
|
||||
developer.log(
|
||||
'🔍 Searching draft postcards with query: "${event.query}"',
|
||||
name: 'MyPostCardBloc',
|
||||
);
|
||||
|
||||
if (state is MyPostCardLoaded) {
|
||||
final currentState = state as MyPostCardLoaded;
|
||||
|
||||
if (event.query.isEmpty) {
|
||||
// If query is empty, show all drafts
|
||||
emit(
|
||||
currentState.copyWith(
|
||||
draftPostCards: currentState.allDraftPostCards,
|
||||
draftSearchQuery: '',
|
||||
),
|
||||
);
|
||||
} else {
|
||||
// Filter the drafts based on the query
|
||||
final filteredDrafts = _filterPostCards(
|
||||
currentState.allDraftPostCards,
|
||||
event.query,
|
||||
);
|
||||
|
||||
developer.log(
|
||||
'✅ Draft search completed: ${filteredDrafts.length} results found',
|
||||
name: 'MyPostCardBloc',
|
||||
);
|
||||
|
||||
emit(
|
||||
currentState.copyWith(
|
||||
draftPostCards: filteredDrafts,
|
||||
draftSearchQuery: event.query,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle searching order postcards
|
||||
Future<void> _onSearchOrderPostCards(
|
||||
SearchOrderPostCards event,
|
||||
Emitter<MyPostCardState> emit,
|
||||
) async {
|
||||
developer.log(
|
||||
'🔍 Searching order postcards with query: "${event.query}"',
|
||||
name: 'MyPostCardBloc',
|
||||
);
|
||||
|
||||
if (state is MyPostCardLoaded) {
|
||||
final currentState = state as MyPostCardLoaded;
|
||||
|
||||
if (event.query.isEmpty) {
|
||||
// If query is empty, show all orders
|
||||
emit(
|
||||
currentState.copyWith(
|
||||
orderPostCards: currentState.allOrderPostCards,
|
||||
orderSearchQuery: '',
|
||||
),
|
||||
);
|
||||
} else {
|
||||
// Filter the orders based on the query
|
||||
final filteredOrders = _filterPostCards(
|
||||
currentState.allOrderPostCards,
|
||||
event.query,
|
||||
);
|
||||
|
||||
developer.log(
|
||||
'✅ Order search completed: ${filteredOrders.length} results found',
|
||||
name: 'MyPostCardBloc',
|
||||
);
|
||||
|
||||
emit(
|
||||
currentState.copyWith(
|
||||
orderPostCards: filteredOrders,
|
||||
orderSearchQuery: event.query,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle clearing draft search
|
||||
Future<void> _onClearDraftSearch(
|
||||
ClearDraftSearch event,
|
||||
Emitter<MyPostCardState> emit,
|
||||
) async {
|
||||
developer.log('🧹 Clearing draft search', name: 'MyPostCardBloc');
|
||||
|
||||
if (state is MyPostCardLoaded) {
|
||||
final currentState = state as MyPostCardLoaded;
|
||||
emit(
|
||||
currentState.copyWith(
|
||||
draftPostCards: currentState.allDraftPostCards,
|
||||
draftSearchQuery: '',
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle clearing order search
|
||||
Future<void> _onClearOrderSearch(
|
||||
ClearOrderSearch event,
|
||||
Emitter<MyPostCardState> emit,
|
||||
) async {
|
||||
developer.log('🧹 Clearing order search', name: 'MyPostCardBloc');
|
||||
|
||||
if (state is MyPostCardLoaded) {
|
||||
final currentState = state as MyPostCardLoaded;
|
||||
emit(
|
||||
currentState.copyWith(
|
||||
orderPostCards: currentState.allOrderPostCards,
|
||||
orderSearchQuery: '',
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper method to filter postcards based on search query
|
||||
/// Searches by title, postcard number, and ID
|
||||
List<MyPostCard> _filterPostCards(
|
||||
List<MyPostCard> postcards,
|
||||
String query,
|
||||
) {
|
||||
final lowerQuery = query.toLowerCase().trim();
|
||||
|
||||
if (lowerQuery.isEmpty) {
|
||||
return postcards;
|
||||
}
|
||||
|
||||
return postcards.where((postcard) {
|
||||
// Search in postcard title (main field)
|
||||
final titleMatch = postcard.pcTitle.toLowerCase().contains(lowerQuery);
|
||||
|
||||
// Search in postcard number
|
||||
final numberMatch = postcard.pcNumber.toString().toLowerCase().contains(lowerQuery);
|
||||
|
||||
// Search in postcard ID
|
||||
final idMatch = postcard.id.toString().contains(lowerQuery);
|
||||
|
||||
// Return true if any field matches
|
||||
return titleMatch || numberMatch || idMatch;
|
||||
}).toList();
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,11 @@ class FetchDraftPostCards extends MyPostCardEvent {
|
||||
const FetchDraftPostCards();
|
||||
}
|
||||
|
||||
class DeleteDraftPostCards extends MyPostCardEvent {
|
||||
final int id;
|
||||
const DeleteDraftPostCards({required this.id});
|
||||
}
|
||||
|
||||
/// Event to fetch order postcards
|
||||
class FetchOrderPostCards extends MyPostCardEvent {
|
||||
const FetchOrderPostCards();
|
||||
@@ -30,4 +35,32 @@ class RefreshDraftPostCards extends MyPostCardEvent {
|
||||
/// Event to refresh order postcards
|
||||
class RefreshOrderPostCards extends MyPostCardEvent {
|
||||
const RefreshOrderPostCards();
|
||||
}
|
||||
|
||||
/// Event to search draft postcards
|
||||
class SearchDraftPostCards extends MyPostCardEvent {
|
||||
final String query;
|
||||
const SearchDraftPostCards({required this.query});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [query];
|
||||
}
|
||||
|
||||
/// Event to search order postcards
|
||||
class SearchOrderPostCards extends MyPostCardEvent {
|
||||
final String query;
|
||||
const SearchOrderPostCards({required this.query});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [query];
|
||||
}
|
||||
|
||||
/// Event to clear draft search
|
||||
class ClearDraftSearch extends MyPostCardEvent {
|
||||
const ClearDraftSearch();
|
||||
}
|
||||
|
||||
/// Event to clear order search
|
||||
class ClearOrderSearch extends MyPostCardEvent {
|
||||
const ClearOrderSearch();
|
||||
}
|
||||
@@ -29,13 +29,26 @@ class MyPostCardLoaded extends MyPostCardState {
|
||||
final List<MyPostCard> orderPostCards;
|
||||
final bool isDraftLoading;
|
||||
final bool isOrderLoading;
|
||||
final bool isDeleteLoading;
|
||||
|
||||
// Search related properties
|
||||
final List<MyPostCard> allDraftPostCards; // Store original unfiltered drafts
|
||||
final List<MyPostCard> allOrderPostCards; // Store original unfiltered orders
|
||||
final String draftSearchQuery;
|
||||
final String orderSearchQuery;
|
||||
|
||||
const MyPostCardLoaded({
|
||||
required this.draftPostCards,
|
||||
required this.orderPostCards,
|
||||
this.isDraftLoading = false,
|
||||
this.isOrderLoading = false,
|
||||
});
|
||||
this.isDeleteLoading = false,
|
||||
List<MyPostCard>? allDraftPostCards,
|
||||
List<MyPostCard>? allOrderPostCards,
|
||||
this.draftSearchQuery = '',
|
||||
this.orderSearchQuery = '',
|
||||
}) : allDraftPostCards = allDraftPostCards ?? draftPostCards,
|
||||
allOrderPostCards = allOrderPostCards ?? orderPostCards;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
@@ -43,6 +56,11 @@ class MyPostCardLoaded extends MyPostCardState {
|
||||
orderPostCards,
|
||||
isDraftLoading,
|
||||
isOrderLoading,
|
||||
isDeleteLoading,
|
||||
allDraftPostCards,
|
||||
allOrderPostCards,
|
||||
draftSearchQuery,
|
||||
orderSearchQuery,
|
||||
];
|
||||
|
||||
/// Helper method to create a copy with updated values
|
||||
@@ -51,12 +69,22 @@ class MyPostCardLoaded extends MyPostCardState {
|
||||
List<MyPostCard>? orderPostCards,
|
||||
bool? isDraftLoading,
|
||||
bool? isOrderLoading,
|
||||
bool? isDeleteLoading,
|
||||
List<MyPostCard>? allDraftPostCards,
|
||||
List<MyPostCard>? allOrderPostCards,
|
||||
String? draftSearchQuery,
|
||||
String? orderSearchQuery,
|
||||
}) {
|
||||
return MyPostCardLoaded(
|
||||
draftPostCards: draftPostCards ?? this.draftPostCards,
|
||||
orderPostCards: orderPostCards ?? this.orderPostCards,
|
||||
isDraftLoading: isDraftLoading ?? this.isDraftLoading,
|
||||
isOrderLoading: isOrderLoading ?? this.isOrderLoading,
|
||||
isDeleteLoading: isDeleteLoading ?? this.isDeleteLoading,
|
||||
allDraftPostCards: allDraftPostCards ?? this.allDraftPostCards,
|
||||
allOrderPostCards: allOrderPostCards ?? this.allOrderPostCards,
|
||||
draftSearchQuery: draftSearchQuery ?? this.draftSearchQuery,
|
||||
orderSearchQuery: orderSearchQuery ?? this.orderSearchQuery,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -66,10 +94,7 @@ class MyPostCardError extends MyPostCardState {
|
||||
final String errorMessage;
|
||||
final String errorType; // 'draft' or 'order'
|
||||
|
||||
const MyPostCardError({
|
||||
required this.errorMessage,
|
||||
required this.errorType,
|
||||
});
|
||||
const MyPostCardError({required this.errorMessage, required this.errorType});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [errorMessage, errorType];
|
||||
|
||||
80
lib/postcard/blocs/pick_images/pick_images_bloc.dart
Normal file
80
lib/postcard/blocs/pick_images/pick_images_bloc.dart
Normal file
@@ -0,0 +1,80 @@
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
|
||||
part 'pick_images_event.dart';
|
||||
part 'pick_images_state.dart';
|
||||
|
||||
class PickImagesBloc extends Bloc<PickImagesEvent, PickImagesState> {
|
||||
PickImagesBloc() : super(PickImagesState()) {
|
||||
final ImagePicker imagePicker = ImagePicker();
|
||||
on<TakePhoto>((event, emit) async {
|
||||
PickImagesState currentState = state;
|
||||
try {
|
||||
emit(currentState.copyWith(loading: true));
|
||||
final XFile? pickedFile = await imagePicker.pickImage(
|
||||
source: ImageSource.camera,
|
||||
imageQuality: 80,
|
||||
maxWidth: 1920,
|
||||
maxHeight: 1920,
|
||||
);
|
||||
|
||||
if (pickedFile != null) {
|
||||
emit(
|
||||
currentState.copyWith(
|
||||
loading: false,
|
||||
file: pickedFile.path,
|
||||
filteredFile: pickedFile.path,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
emit(currentState.copyWith(loading: false));
|
||||
}
|
||||
} catch (e) {
|
||||
emit(currentState.copyWith(loading: false));
|
||||
}
|
||||
});
|
||||
|
||||
on<PickPhoto>((event, emit) async {
|
||||
PickImagesState currentState = state;
|
||||
try {
|
||||
emit(currentState.copyWith(loading: true));
|
||||
final XFile? pickedFile = await imagePicker.pickImage(
|
||||
source: ImageSource.gallery,
|
||||
imageQuality: 80,
|
||||
maxWidth: 1920,
|
||||
maxHeight: 1920,
|
||||
);
|
||||
|
||||
if (pickedFile != null) {
|
||||
emit(
|
||||
currentState.copyWith(
|
||||
loading: false,
|
||||
file: pickedFile.path,
|
||||
filteredFile: pickedFile.path,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
emit(currentState.copyWith(loading: false));
|
||||
}
|
||||
} catch (e) {
|
||||
emit(currentState.copyWith(loading: false));
|
||||
}
|
||||
});
|
||||
|
||||
on<SelectedFilter>((event, emit) async {
|
||||
PickImagesState currentState = state;
|
||||
try {
|
||||
emit(
|
||||
currentState.copyWith(
|
||||
loading: false,
|
||||
file: currentState.file ?? event.imagePath,
|
||||
filteredFile: event.imagePath,
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
emit(currentState.copyWith(loading: false));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
19
lib/postcard/blocs/pick_images/pick_images_event.dart
Normal file
19
lib/postcard/blocs/pick_images/pick_images_event.dart
Normal file
@@ -0,0 +1,19 @@
|
||||
part of 'pick_images_bloc.dart';
|
||||
|
||||
class PickImagesEvent extends Equatable {
|
||||
const PickImagesEvent();
|
||||
|
||||
@override
|
||||
List<Object> get props => [];
|
||||
}
|
||||
|
||||
class TakePhoto extends PickImagesEvent {}
|
||||
|
||||
class PickPhoto extends PickImagesEvent {}
|
||||
|
||||
class RemovePhoto extends PickImagesEvent {}
|
||||
|
||||
class SelectedFilter extends PickImagesEvent {
|
||||
final String imagePath;
|
||||
const SelectedFilter({required this.imagePath});
|
||||
}
|
||||
23
lib/postcard/blocs/pick_images/pick_images_state.dart
Normal file
23
lib/postcard/blocs/pick_images/pick_images_state.dart
Normal file
@@ -0,0 +1,23 @@
|
||||
part of 'pick_images_bloc.dart';
|
||||
|
||||
class PickImagesState extends Equatable {
|
||||
final String? file;
|
||||
final String? filteredFile;
|
||||
final bool? loading;
|
||||
const PickImagesState({this.file, this.loading = false, this.filteredFile});
|
||||
|
||||
PickImagesState copyWith({
|
||||
String? file,
|
||||
bool? loading,
|
||||
String? filteredFile,
|
||||
}) {
|
||||
return PickImagesState(
|
||||
file: file ?? this.file,
|
||||
loading: loading ?? this.loading,
|
||||
filteredFile: filteredFile ?? this.filteredFile,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<dynamic> get props => [file, filteredFile, loading];
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'dart:developer';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../../repository/postcard_checkout_repository.dart';
|
||||
import 'postcard_checkout_event.dart';
|
||||
@@ -14,62 +15,76 @@ class PostcardCheckoutBloc
|
||||
on<UpdateCheckoutDataEvent>(_onUpdateCheckoutData);
|
||||
on<SaveAsDraftEvent>(_onSaveAsDraft);
|
||||
on<SubmitPostcardEvent>(_onSubmitPostcard);
|
||||
on<ConfirmPaymentEvent>(_onConfirmPayment); // 🆕 NEW
|
||||
on<ConfirmPaymentEvent>(_onConfirmPayment);
|
||||
}
|
||||
|
||||
void _onUpdateAddress(
|
||||
UpdateAddressEvent event, Emitter<PostcardCheckoutState> emit) {
|
||||
emit(state.copyWith(
|
||||
countryName: event.countryName,
|
||||
cityName: event.cityName,
|
||||
stateName: event.stateName,
|
||||
zipCode: event.zipCode,
|
||||
address1: event.address1,
|
||||
address2: event.address2,
|
||||
));
|
||||
UpdateAddressEvent event,
|
||||
Emitter<PostcardCheckoutState> emit,
|
||||
) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
countryName: event.countryName,
|
||||
cityName: event.cityName,
|
||||
stateName: event.stateName,
|
||||
zipCode: event.zipCode,
|
||||
address1: event.address1,
|
||||
address2: event.address2,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _onUpdateContent(
|
||||
UpdatePostcardContentEvent event, Emitter<PostcardCheckoutState> emit) {
|
||||
emit(state.copyWith(
|
||||
pcTitle: event.pcTitle,
|
||||
pcContent: event.pcContent,
|
||||
pcImageFile: event.pcImageFile,
|
||||
));
|
||||
UpdatePostcardContentEvent event,
|
||||
Emitter<PostcardCheckoutState> emit,
|
||||
) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
pcTitle: event.pcTitle,
|
||||
pcContent: event.pcContent,
|
||||
pcImageFile: event.pcImageFile,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _onUpdateCheckoutData(
|
||||
UpdateCheckoutDataEvent event, Emitter<PostcardCheckoutState> emit) {
|
||||
emit(state.copyWith(
|
||||
countryName: event.countryName,
|
||||
cityName: event.cityName,
|
||||
stateName: event.stateName,
|
||||
zipCode: event.zipCode,
|
||||
address1: event.address1,
|
||||
address2: event.address2,
|
||||
pcTitle: event.pcTitle,
|
||||
pcContent: event.pcContent,
|
||||
pcImageFile: event.pcImageFile,
|
||||
pcNumber: event.pcNumber,
|
||||
pcDatetime: event.pcDatetime,
|
||||
fullname: event.fullname,
|
||||
emailAddress: event.emailAddress,
|
||||
mobileNumber: event.mobileNumber,
|
||||
isdCode: event.isdCode,
|
||||
isForSelf: event.isForSelf,
|
||||
baseAmount: event.baseAmount,
|
||||
totalTaxAmount: event.totalTaxAmount,
|
||||
totalAmount: event.totalAmount,
|
||||
postcardId: event.postcardId,
|
||||
));
|
||||
UpdateCheckoutDataEvent event,
|
||||
Emitter<PostcardCheckoutState> emit,
|
||||
) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
countryName: event.countryName,
|
||||
cityName: event.cityName,
|
||||
stateName: event.stateName,
|
||||
zipCode: event.zipCode,
|
||||
address1: event.address1,
|
||||
address2: event.address2,
|
||||
pcTitle: event.pcTitle,
|
||||
pcContent: event.pcContent,
|
||||
pcImageFile: event.pcImageFile,
|
||||
pcNumber: event.pcNumber,
|
||||
pcDatetime: event.pcDatetime,
|
||||
fullname: event.fullname,
|
||||
emailAddress: event.emailAddress,
|
||||
mobileNumber: event.mobileNumber,
|
||||
isdCode: event.isdCode,
|
||||
isForSelf: event.isForSelf,
|
||||
baseAmount: event.baseAmount,
|
||||
totalTaxAmount: event.totalTaxAmount,
|
||||
totalAmount: event.totalAmount,
|
||||
postcardId: event.postcardId,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _onSaveAsDraft(
|
||||
SaveAsDraftEvent event, Emitter<PostcardCheckoutState> emit) async {
|
||||
SaveAsDraftEvent event,
|
||||
Emitter<PostcardCheckoutState> emit,
|
||||
) async {
|
||||
emit(state.copyWith(isLoading: true, error: null, isSuccess: false));
|
||||
|
||||
try {
|
||||
// ⭐ Validate pcId exists
|
||||
// Validate pcId exists
|
||||
if (state.postcardId == null) {
|
||||
emit(state.copyWith(
|
||||
isLoading: false,
|
||||
@@ -79,9 +94,21 @@ class PostcardCheckoutBloc
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate that image file exists before submitting
|
||||
if (state.pcImageFile == null) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
isLoading: false,
|
||||
error: 'Please select a postcard image',
|
||||
isSuccess: false,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final response = await repository.createPostCard(
|
||||
pcId: state.postcardId!,
|
||||
isDraft: true, // ⭐ Save as draft
|
||||
isDraft: true,
|
||||
);
|
||||
|
||||
// Extract order ID from response if available
|
||||
@@ -89,27 +116,33 @@ class PostcardCheckoutBloc
|
||||
response['order_id']?.toString() ??
|
||||
response['id']?.toString();
|
||||
|
||||
emit(state.copyWith(
|
||||
isLoading: false,
|
||||
isSuccess: true,
|
||||
isDraft: true,
|
||||
orderId: orderId,
|
||||
));
|
||||
emit(
|
||||
state.copyWith(
|
||||
isLoading: false,
|
||||
isSuccess: true,
|
||||
isDraft: true,
|
||||
orderId: orderId,
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
emit(state.copyWith(
|
||||
isLoading: false,
|
||||
error: e.toString(),
|
||||
isSuccess: false,
|
||||
));
|
||||
emit(
|
||||
state.copyWith(
|
||||
isLoading: false,
|
||||
error: e.toString(),
|
||||
isSuccess: false,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onSubmitPostcard(
|
||||
SubmitPostcardEvent event, Emitter<PostcardCheckoutState> emit) async {
|
||||
SubmitPostcardEvent event,
|
||||
Emitter<PostcardCheckoutState> emit,
|
||||
) async {
|
||||
emit(state.copyWith(isLoading: true, error: null, isSuccess: false));
|
||||
|
||||
try {
|
||||
// ⭐ Validate pcId exists
|
||||
// Validate pcId exists
|
||||
if (state.postcardId == null) {
|
||||
emit(state.copyWith(
|
||||
isLoading: false,
|
||||
@@ -119,65 +152,92 @@ class PostcardCheckoutBloc
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate that image file exists before submitting
|
||||
if (state.pcImageFile == null) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
isLoading: false,
|
||||
error: 'Please select a postcard image',
|
||||
isSuccess: false,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final response = await repository.createPostCard(
|
||||
pcId: state.postcardId!,
|
||||
isDraft: false, // ⭐ Initiate payment
|
||||
isDraft: false,
|
||||
);
|
||||
|
||||
// Parse response from backend
|
||||
final postcardId = response['postcardId'] as int?;
|
||||
final clientSecret = response['clientSecret'] as String?;
|
||||
final status = response['status'] as String?;
|
||||
|
||||
// Extract order ID from response
|
||||
final orderId = response['orderId']?.toString() ??
|
||||
response['order_id']?.toString() ??
|
||||
response['id']?.toString();
|
||||
|
||||
// Validate clientSecret is present
|
||||
if (clientSecret == null || clientSecret.isEmpty) {
|
||||
emit(state.copyWith(
|
||||
isLoading: false,
|
||||
error: 'Payment initialization failed - no client secret received',
|
||||
isSuccess: false,
|
||||
));
|
||||
emit(
|
||||
state.copyWith(
|
||||
isLoading: false,
|
||||
error: 'Payment initialization failed - no client secret received',
|
||||
isSuccess: false,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
emit(state.copyWith(
|
||||
isLoading: false,
|
||||
isSuccess: true,
|
||||
isDraft: false,
|
||||
postcardId: postcardId ?? state.postcardId,
|
||||
clientSecret: clientSecret,
|
||||
orderId: orderId,
|
||||
));
|
||||
} catch (e) {
|
||||
emit(state.copyWith(
|
||||
isLoading: false,
|
||||
error: e.toString(),
|
||||
isSuccess: false,
|
||||
));
|
||||
// Emit success with clientSecret for payment processing
|
||||
emit(
|
||||
state.copyWith(
|
||||
isLoading: false,
|
||||
isSuccess: true,
|
||||
isDraft: false,
|
||||
postcardId: postcardId ?? state.postcardId,
|
||||
clientSecret: clientSecret,
|
||||
orderId: orderId,
|
||||
),
|
||||
);
|
||||
} catch (e, stack) {
|
||||
log("Payment Error: ${e.toString()}");
|
||||
log("Payment Stack: ${stack.toString()}");
|
||||
emit(
|
||||
state.copyWith(
|
||||
isLoading: false,
|
||||
error: e.toString(),
|
||||
isSuccess: false,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 🆕 Confirm payment after Stripe payment completes
|
||||
/// This should be called after Stripe payment succeeds or fails
|
||||
/// Confirm payment after Stripe payment completes
|
||||
Future<void> _onConfirmPayment(
|
||||
ConfirmPaymentEvent event, Emitter<PostcardCheckoutState> emit) async {
|
||||
|
||||
ConfirmPaymentEvent event,
|
||||
Emitter<PostcardCheckoutState> emit,
|
||||
) async {
|
||||
// Validate postcardId exists
|
||||
if (state.postcardId == null) {
|
||||
emit(state.copyWith(
|
||||
confirmationError: 'Cannot confirm payment - postcard ID is missing',
|
||||
isConfirmingPayment: false,
|
||||
isPaymentConfirmed: false,
|
||||
));
|
||||
emit(
|
||||
state.copyWith(
|
||||
confirmationError: 'Cannot confirm payment - postcard ID is missing',
|
||||
isConfirmingPayment: false,
|
||||
isPaymentConfirmed: false,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
emit(state.copyWith(
|
||||
isConfirmingPayment: true,
|
||||
confirmationError: null,
|
||||
isPaymentConfirmed: false,
|
||||
));
|
||||
emit(
|
||||
state.copyWith(
|
||||
isConfirmingPayment: true,
|
||||
confirmationError: null,
|
||||
isPaymentConfirmed: false,
|
||||
),
|
||||
);
|
||||
|
||||
try {
|
||||
final response = await repository.confirmPayment(
|
||||
@@ -187,17 +247,21 @@ class PostcardCheckoutBloc
|
||||
);
|
||||
|
||||
// Payment confirmation successful
|
||||
emit(state.copyWith(
|
||||
isConfirmingPayment: false,
|
||||
isPaymentConfirmed: true,
|
||||
confirmationError: null,
|
||||
));
|
||||
emit(
|
||||
state.copyWith(
|
||||
isConfirmingPayment: false,
|
||||
isPaymentConfirmed: true,
|
||||
confirmationError: null,
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
emit(state.copyWith(
|
||||
isConfirmingPayment: false,
|
||||
isPaymentConfirmed: false,
|
||||
confirmationError: e.toString(),
|
||||
));
|
||||
emit(
|
||||
state.copyWith(
|
||||
isConfirmingPayment: false,
|
||||
isPaymentConfirmed: false,
|
||||
confirmationError: e.toString(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -170,4 +170,81 @@ class MyPostCard {
|
||||
'updatedAt': updatedAt.toIso8601String(),
|
||||
};
|
||||
}
|
||||
|
||||
MyPostCard copyWith({
|
||||
int? id,
|
||||
int? userXid,
|
||||
String? pcTitle,
|
||||
String? pcNumber,
|
||||
String? cityName,
|
||||
DateTime? pcDatetime,
|
||||
String? pcContent,
|
||||
String? pcImagePath,
|
||||
bool? isForSelf,
|
||||
String? fullname,
|
||||
String? emailAddress,
|
||||
String? isdCode,
|
||||
String? mobileNumber,
|
||||
String? address1,
|
||||
String? address2,
|
||||
String? zipCode,
|
||||
String? stateName,
|
||||
String? countryName,
|
||||
String? orderStatus,
|
||||
double? baseAmount,
|
||||
int? couponXid,
|
||||
double? couponDiscountPercent,
|
||||
double? couponDiscountAmount,
|
||||
double? totalTaxAmount,
|
||||
double? totalAmount,
|
||||
bool? isPaid,
|
||||
String? paymentMode,
|
||||
String? paymentId,
|
||||
String? paymentStatus,
|
||||
String? paymentIntentId,
|
||||
bool? isDraft,
|
||||
DateTime? deliveredOn,
|
||||
bool? isActive,
|
||||
DateTime? createdAt,
|
||||
DateTime? updatedAt,
|
||||
}) {
|
||||
return MyPostCard(
|
||||
id: id ?? this.id,
|
||||
userXid: userXid ?? this.userXid,
|
||||
pcTitle: pcTitle ?? this.pcTitle,
|
||||
pcNumber: pcNumber ?? this.pcNumber,
|
||||
cityName: cityName ?? this.cityName,
|
||||
pcDatetime: pcDatetime ?? this.pcDatetime,
|
||||
pcContent: pcContent ?? this.pcContent,
|
||||
pcImagePath: pcImagePath ?? this.pcImagePath,
|
||||
isForSelf: isForSelf ?? this.isForSelf,
|
||||
fullname: fullname ?? this.fullname,
|
||||
emailAddress: emailAddress ?? this.emailAddress,
|
||||
isdCode: isdCode ?? this.isdCode,
|
||||
mobileNumber: mobileNumber ?? this.mobileNumber,
|
||||
address1: address1 ?? this.address1,
|
||||
address2: address2 ?? this.address2,
|
||||
zipCode: zipCode ?? this.zipCode,
|
||||
stateName: stateName ?? this.stateName,
|
||||
countryName: countryName ?? this.countryName,
|
||||
orderStatus: orderStatus ?? this.orderStatus,
|
||||
baseAmount: baseAmount ?? this.baseAmount,
|
||||
couponXid: couponXid ?? this.couponXid,
|
||||
couponDiscountPercent:
|
||||
couponDiscountPercent ?? this.couponDiscountPercent,
|
||||
couponDiscountAmount: couponDiscountAmount ?? this.couponDiscountAmount,
|
||||
totalTaxAmount: totalTaxAmount ?? this.totalTaxAmount,
|
||||
totalAmount: totalAmount ?? this.totalAmount,
|
||||
isPaid: isPaid ?? this.isPaid,
|
||||
paymentMode: paymentMode ?? this.paymentMode,
|
||||
paymentId: paymentId ?? this.paymentId,
|
||||
paymentStatus: paymentStatus ?? this.paymentStatus,
|
||||
paymentIntentId: paymentIntentId ?? this.paymentIntentId,
|
||||
isDraft: isDraft ?? this.isDraft,
|
||||
deliveredOn: deliveredOn ?? this.deliveredOn,
|
||||
isActive: isActive ?? this.isActive,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
updatedAt: updatedAt ?? this.updatedAt,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
|
||||
import '../../networkApiServices/network_api_services.dart';
|
||||
import '../../networkApiServices/api_urls.dart';
|
||||
import '../models/my_postcard_model.dart';
|
||||
@@ -13,8 +17,63 @@ class MyPostCardsRepository {
|
||||
url: '${ApiUrls.myPostCards}?type=$type',
|
||||
);
|
||||
|
||||
return (response.data as List)
|
||||
.map((e) => MyPostCard.fromJson(e))
|
||||
.toList();
|
||||
return (response.data as List).map((e) => MyPostCard.fromJson(e)).toList();
|
||||
}
|
||||
|
||||
Future<void> editMyPostCards({
|
||||
required MyPostCard postcard,
|
||||
String? image,
|
||||
}) async {
|
||||
try {
|
||||
final formData = FormData();
|
||||
|
||||
formData.fields.addAll([
|
||||
MapEntry('countryName', postcard.countryName),
|
||||
MapEntry('cityName', postcard.cityName),
|
||||
MapEntry('stateName', postcard.stateName),
|
||||
MapEntry('zipCode', postcard.zipCode),
|
||||
MapEntry('pcTitle', postcard.pcTitle),
|
||||
MapEntry('pcContent', postcard.pcContent),
|
||||
MapEntry('pcNumber', postcard.pcNumber),
|
||||
MapEntry('pcDatetime', postcard.pcDatetime.toString()),
|
||||
MapEntry('fullname', postcard.fullname),
|
||||
MapEntry('isdCode', postcard.isdCode),
|
||||
]);
|
||||
|
||||
if (postcard.address1.isNotEmpty) {
|
||||
formData.fields.add(MapEntry('address1', postcard.address1));
|
||||
}
|
||||
|
||||
if (postcard.address2.isNotEmpty) {
|
||||
formData.fields.add(MapEntry('address2', postcard.address2));
|
||||
}
|
||||
if (image != null && image.isNotEmpty) {
|
||||
final fileName = image.split('/').last;
|
||||
formData.files.add(
|
||||
MapEntry(
|
||||
'pcImage',
|
||||
await MultipartFile.fromFile(image, filename: fileName),
|
||||
),
|
||||
);
|
||||
}
|
||||
await _apiService.putApi(
|
||||
url: '${ApiUrls.editPostcard}/${postcard.id}',
|
||||
data: formData,
|
||||
);
|
||||
return;
|
||||
} catch (e, stack) {
|
||||
log("Edit PostCard Error - $e");
|
||||
log("Edit PostCard Error - $stack");
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> deleteMyPostCards(int id) async {
|
||||
try {
|
||||
await _apiService.deleteApi(url: '${ApiUrls.editPostcard}/$id');
|
||||
return;
|
||||
} catch (e, stack) {
|
||||
log("Delete PostCard Error - $e");
|
||||
log("Delete PostCard Error - $stack");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
424
lib/postcard/views/edit_image_filter.dart
Normal file
424
lib/postcard/views/edit_image_filter.dart
Normal file
@@ -0,0 +1,424 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:citycards_customer/common_packages/app_bar.dart';
|
||||
import 'package:citycards_customer/postcard/blocs/edit_image_filter/edit_image_filter_bloc.dart';
|
||||
import 'package:citycards_customer/postcard/blocs/pick_images/pick_images_bloc.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
|
||||
import '../widgets/dotted_border_holder.dart';
|
||||
|
||||
class EditImageFilter extends StatefulWidget {
|
||||
final EditImageType type;
|
||||
final String url;
|
||||
final PickImagesBloc pickImagesBloc;
|
||||
const EditImageFilter({
|
||||
super.key,
|
||||
required this.type,
|
||||
required this.url,
|
||||
required this.pickImagesBloc,
|
||||
});
|
||||
|
||||
@override
|
||||
State<EditImageFilter> createState() => _EditImageFilterState();
|
||||
}
|
||||
|
||||
class _EditImageFilterState extends State<EditImageFilter> {
|
||||
final EditImageFilterBloc editImageFilterBloc = EditImageFilterBloc();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
editImageFilterBloc.add(DownloadImage(url: widget.url, type: widget.type));
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocConsumer<EditImageFilterBloc, EditImageFilterState>(
|
||||
bloc: editImageFilterBloc,
|
||||
listener: (ctx, state) {
|
||||
if (state is DownloadImageFailed) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text("Failed to fetch edit details")),
|
||||
);
|
||||
Navigator.pop(context);
|
||||
}
|
||||
},
|
||||
builder: (context, state) {
|
||||
if (state is DownloadImageLoading) {
|
||||
return const Scaffold(
|
||||
body: SafeArea(child: Center(child: CircularProgressIndicator())),
|
||||
);
|
||||
}
|
||||
|
||||
if (state is DownloadImageSuccessfully) {
|
||||
return Scaffold(
|
||||
body: SafeArea(
|
||||
child: Stack(
|
||||
children: [
|
||||
SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
CommonAppBar(
|
||||
isWhiteLogo: false,
|
||||
isProfilePage: false,
|
||||
showDivider: true,
|
||||
),
|
||||
GestureDetector(
|
||||
onTap: () => Navigator.pop(context),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.arrow_back, size: 20),
|
||||
const SizedBox(width: 8),
|
||||
const Text(
|
||||
"Back",
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const Text(
|
||||
"Add a Filter",
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
"Choose your favorite filter and enhance your postcard.",
|
||||
style: TextStyle(
|
||||
fontSize: 14.sp,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: const Color(0xff2D3134),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
DottedBorderContainerHolder(
|
||||
imagePath: state.filteredImagePath,
|
||||
filter: state.filter,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(
|
||||
children: [
|
||||
buildFilterOption(
|
||||
context,
|
||||
"Original",
|
||||
File(state.filePath),
|
||||
"original",
|
||||
state.filter == "original",
|
||||
),
|
||||
buildFilterOption(
|
||||
context,
|
||||
"Black & White",
|
||||
File(state.filePath),
|
||||
"bw",
|
||||
state.filter == "bw",
|
||||
),
|
||||
buildFilterOption(
|
||||
context,
|
||||
"Sepia",
|
||||
File(state.filePath),
|
||||
"sepia",
|
||||
state.filter == "sepia",
|
||||
),
|
||||
buildFilterOption(
|
||||
context,
|
||||
"Vintage",
|
||||
File(state.filePath),
|
||||
"vintage",
|
||||
state.filter == "vintage",
|
||||
),
|
||||
buildFilterOption(
|
||||
context,
|
||||
"Cool Tone",
|
||||
File(state.filePath),
|
||||
"cool",
|
||||
state.filter == "cool",
|
||||
),
|
||||
buildFilterOption(
|
||||
context,
|
||||
"Contrast",
|
||||
File(state.filePath),
|
||||
"contrast",
|
||||
state.filter == "contrast",
|
||||
),
|
||||
buildFilterOption(
|
||||
context,
|
||||
"Soft Glow",
|
||||
File(state.filePath),
|
||||
"soft",
|
||||
state.filter == "soft",
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(height: 20.h),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xffF95F62),
|
||||
padding: EdgeInsets.symmetric(vertical: 16.h),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(40),
|
||||
),
|
||||
),
|
||||
onPressed: () {
|
||||
widget.pickImagesBloc.add(
|
||||
SelectedFilter(
|
||||
imagePath: state.filteredImagePath,
|
||||
),
|
||||
);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: Text(
|
||||
"Save Changes",
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 14.sp,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Processing overlay
|
||||
if (state.processing == true)
|
||||
Container(
|
||||
color: Colors.black.withValues(alpha: .4),
|
||||
child: const Center(
|
||||
child: CircularProgressIndicator(
|
||||
color: Color(0xffF95F62),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return const Scaffold(
|
||||
body: SafeArea(child: Center(child: CircularProgressIndicator())),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Builds a single filter preview thumbnail
|
||||
Widget buildFilterOption(
|
||||
BuildContext context,
|
||||
String label,
|
||||
File imageFile,
|
||||
String filter,
|
||||
bool isSelected,
|
||||
) {
|
||||
return GestureDetector(
|
||||
onTap: () => editImageFilterBloc.add(SelectFilter(filterName: filter)),
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(right: 12),
|
||||
width: 90,
|
||||
child: Column(
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
child: ColorFiltered(
|
||||
colorFilter: getColorFilter(filter),
|
||||
child: Image.file(
|
||||
imageFile,
|
||||
height: 70,
|
||||
width: 90,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
label,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: isSelected
|
||||
? const Color(0xffF95F62)
|
||||
: const Color(0xff2D3134),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
ColorFilter getColorFilter(String? filter) {
|
||||
switch (filter) {
|
||||
case "vintage":
|
||||
// Muted, warm tones without overflow
|
||||
return const ColorFilter.matrix([
|
||||
0.9,
|
||||
0.3,
|
||||
0.1,
|
||||
0,
|
||||
0,
|
||||
0.2,
|
||||
0.8,
|
||||
0.1,
|
||||
0,
|
||||
0,
|
||||
0.1,
|
||||
0.3,
|
||||
0.7,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
]);
|
||||
|
||||
case "bw":
|
||||
// Grayscale
|
||||
return const ColorFilter.matrix([
|
||||
0.2126,
|
||||
0.7152,
|
||||
0.0722,
|
||||
0,
|
||||
0,
|
||||
0.2126,
|
||||
0.7152,
|
||||
0.0722,
|
||||
0,
|
||||
0,
|
||||
0.2126,
|
||||
0.7152,
|
||||
0.0722,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
]);
|
||||
|
||||
case "sepia":
|
||||
// Classic soft brown
|
||||
return const ColorFilter.matrix([
|
||||
0.393,
|
||||
0.769,
|
||||
0.189,
|
||||
0,
|
||||
0,
|
||||
0.349,
|
||||
0.686,
|
||||
0.168,
|
||||
0,
|
||||
0,
|
||||
0.272,
|
||||
0.534,
|
||||
0.131,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
]);
|
||||
|
||||
case "cool":
|
||||
// Gentle blue tone — no gamma boost to avoid clipping
|
||||
return const ColorFilter.matrix([
|
||||
1.0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1.0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1.1,
|
||||
0,
|
||||
10,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
]);
|
||||
|
||||
case "contrast":
|
||||
// Slight contrast increase, safe range
|
||||
return const ColorFilter.matrix([
|
||||
1.1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
-10,
|
||||
0,
|
||||
1.1,
|
||||
0,
|
||||
0,
|
||||
-10,
|
||||
0,
|
||||
0,
|
||||
1.1,
|
||||
0,
|
||||
-10,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
]);
|
||||
|
||||
case "soft":
|
||||
// Gentle brightness and warmth — fixed to avoid pixelation
|
||||
return const ColorFilter.matrix([
|
||||
1.02,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
5,
|
||||
0,
|
||||
1.02,
|
||||
0,
|
||||
0,
|
||||
5,
|
||||
0,
|
||||
0,
|
||||
1.02,
|
||||
0,
|
||||
5,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
]);
|
||||
|
||||
default:
|
||||
return const ColorFilter.mode(Colors.transparent, BlendMode.srcOver);
|
||||
}
|
||||
}
|
||||
}
|
||||
510
lib/postcard/views/edit_postcard_view.dart
Normal file
510
lib/postcard/views/edit_postcard_view.dart
Normal file
@@ -0,0 +1,510 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:citycards_customer/postcard/blocs/edit_image_filter/edit_image_filter_bloc.dart';
|
||||
import 'package:citycards_customer/postcard/blocs/edit_postcard/edit_postcard_bloc.dart';
|
||||
import 'package:citycards_customer/postcard/blocs/pick_images/pick_images_bloc.dart';
|
||||
import 'package:citycards_customer/postcard/models/my_postcard_model.dart';
|
||||
import 'package:citycards_customer/postcard/widgets/dotted_border_container.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
|
||||
import '../../common_packages/app_bar.dart';
|
||||
import '../../common_packages/custom_text.dart';
|
||||
import '../../networkApiServices/api_urls.dart';
|
||||
import '../widgets/edit_post_card/edit_message.dart';
|
||||
import '../widgets/edit_post_card/your_details.dart';
|
||||
import 'edit_image_filter.dart';
|
||||
|
||||
class EditPostcardView extends StatefulWidget {
|
||||
final MyPostCard myPostCard;
|
||||
const EditPostcardView({super.key, required this.myPostCard});
|
||||
|
||||
@override
|
||||
State<EditPostcardView> createState() => _EditPostcardViewState();
|
||||
}
|
||||
|
||||
class _EditPostcardViewState extends State<EditPostcardView> {
|
||||
MyPostCard? postCard;
|
||||
final EditPostcardBloc editPostcardBloc = EditPostcardBloc();
|
||||
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
|
||||
final _fullNameController = TextEditingController();
|
||||
final _addressController = TextEditingController();
|
||||
final _cityController = TextEditingController();
|
||||
final _zipCodeController = TextEditingController();
|
||||
|
||||
String? _selectedCountry;
|
||||
String? _selectedState;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_fullNameController.dispose();
|
||||
_addressController.dispose();
|
||||
_cityController.dispose();
|
||||
_zipCodeController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
setState(() {
|
||||
postCard = widget.myPostCard;
|
||||
_fullNameController.text = widget.myPostCard.fullname;
|
||||
_addressController.text = widget.myPostCard.address1;
|
||||
_cityController.text = widget.myPostCard.cityName;
|
||||
_zipCodeController.text = widget.myPostCard.zipCode;
|
||||
_selectedCountry = widget.myPostCard.countryName;
|
||||
_selectedState = widget.myPostCard.stateName;
|
||||
});
|
||||
super.initState();
|
||||
}
|
||||
|
||||
String? selectedImage;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final size = MediaQuery.of(context).size;
|
||||
return Scaffold(
|
||||
resizeToAvoidBottomInset: true,
|
||||
backgroundColor: Colors.white,
|
||||
body: BlocConsumer<EditPostcardBloc, EditPostcardState>(
|
||||
bloc: editPostcardBloc,
|
||||
listener: (ctxx, state) async {
|
||||
if (state is EditPostcardSuccessfull) {
|
||||
if (Navigator.canPop(ctxx)) {
|
||||
Navigator.pop(ctxx, true);
|
||||
}
|
||||
} else if (state is EditPostcardError) {
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text(state.error)));
|
||||
}
|
||||
},
|
||||
builder: (context, state) {
|
||||
return Stack(
|
||||
children: [
|
||||
SafeArea(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 20.w),
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
CommonAppBar(
|
||||
isWhiteLogo: false,
|
||||
isProfilePage: false,
|
||||
showCart: true,
|
||||
showDivider: true,
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: () => Navigator.pop(context),
|
||||
child: const Icon(Icons.arrow_back),
|
||||
),
|
||||
SizedBox(width: 8.w),
|
||||
CustomText(text: "Edit Postcard", size: 12.sp),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 10.h),
|
||||
Text(
|
||||
"Upload Image",
|
||||
style: GoogleFonts.poppins(
|
||||
color: Color(0XFF212121),
|
||||
fontSize: 18.sp,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 2.h),
|
||||
Text(
|
||||
"Edit your own unique postcards by uploading images that capture your unforgettable moments.",
|
||||
style: GoogleFonts.poppins(
|
||||
color: Color(0XFF000000).withValues(alpha: 0.6),
|
||||
fontSize: 14.sp,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 10.h),
|
||||
BlocConsumer<PickImagesBloc, PickImagesState>(
|
||||
listener: (ctx, state) {
|
||||
if (state.file != null && state.file!.isNotEmpty) {
|
||||
setState(() {
|
||||
selectedImage =
|
||||
state.filteredFile ?? state.file!;
|
||||
});
|
||||
}
|
||||
},
|
||||
builder: (context, state) {
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: CustomPaint(
|
||||
painter: DottedBorderPainter(),
|
||||
child: Container(
|
||||
padding: EdgeInsets.all(10),
|
||||
height: size.width * 0.45,
|
||||
width: size.width,
|
||||
constraints: BoxConstraints(
|
||||
minHeight: 150,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
child:
|
||||
state.file != null &&
|
||||
state.file!.isNotEmpty
|
||||
? Image.file(
|
||||
height: size.width * 0.45,
|
||||
width: size.width,
|
||||
fit: BoxFit.cover,
|
||||
File(
|
||||
state.filteredFile ??
|
||||
state.file!,
|
||||
),
|
||||
)
|
||||
: Stack(
|
||||
children: [
|
||||
Image.network(
|
||||
'${ApiUrls.baseUrl}${postCard!.pcImagePath}',
|
||||
height: size.width * 0.45,
|
||||
width: size.width,
|
||||
fit: BoxFit.cover,
|
||||
loadingBuilder:
|
||||
(
|
||||
context,
|
||||
child,
|
||||
loadingProgress,
|
||||
) {
|
||||
if (loadingProgress ==
|
||||
null) {
|
||||
return child;
|
||||
}
|
||||
return Container(
|
||||
height:
|
||||
size.width *
|
||||
0.45,
|
||||
width: size.width,
|
||||
color: Colors
|
||||
.grey[300],
|
||||
child: const Center(
|
||||
child:
|
||||
CircularProgressIndicator(
|
||||
color: Color(
|
||||
0xffF95F62,
|
||||
),
|
||||
strokeWidth:
|
||||
2,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
errorBuilder:
|
||||
(
|
||||
context,
|
||||
error,
|
||||
stackTrace,
|
||||
) {
|
||||
return Container(
|
||||
height:
|
||||
size.width *
|
||||
0.45,
|
||||
width: size.width,
|
||||
color: Colors
|
||||
.grey[300],
|
||||
child: const Icon(
|
||||
Icons
|
||||
.image_not_supported,
|
||||
color:
|
||||
Colors.grey,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
Positioned(
|
||||
child: state.loading == true
|
||||
? Container(
|
||||
height:
|
||||
size.width *
|
||||
0.45,
|
||||
width: size.width,
|
||||
decoration:
|
||||
BoxDecoration(
|
||||
color: Colors
|
||||
.black
|
||||
.withValues(
|
||||
alpha:
|
||||
0.25,
|
||||
),
|
||||
),
|
||||
child: Center(
|
||||
child: SizedBox(
|
||||
height: 25,
|
||||
width: 25,
|
||||
child: CircularProgressIndicator(
|
||||
color: Colors
|
||||
.white,
|
||||
strokeWidth:
|
||||
2,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
: SizedBox(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Container(
|
||||
height: size.width * 0.5,
|
||||
width: size.width,
|
||||
constraints: BoxConstraints(minHeight: 150),
|
||||
padding: EdgeInsets.all(10),
|
||||
child: Column(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
imageButton(
|
||||
title: 'Take a photo',
|
||||
icon: Icons.camera_alt_outlined,
|
||||
width: size.width,
|
||||
onPressed: () {
|
||||
context.read<PickImagesBloc>().add(
|
||||
TakePhoto(),
|
||||
);
|
||||
},
|
||||
),
|
||||
imageButton(
|
||||
title: 'Upload Again',
|
||||
icon: Icons.refresh,
|
||||
width: size.width,
|
||||
onPressed: () {
|
||||
context.read<PickImagesBloc>().add(
|
||||
PickPhoto(),
|
||||
);
|
||||
},
|
||||
),
|
||||
imageButton(
|
||||
title: 'Edit Filters',
|
||||
width: size.width,
|
||||
onPressed: () {
|
||||
final pickImagesBloc = context
|
||||
.read<PickImagesBloc>();
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => BlocProvider(
|
||||
create: (context) =>
|
||||
EditImageFilterBloc(),
|
||||
child: EditImageFilter(
|
||||
type:
|
||||
state.file != null &&
|
||||
state
|
||||
.file!
|
||||
.isNotEmpty
|
||||
? EditImageType.file
|
||||
: EditImageType.network,
|
||||
url:
|
||||
state.file != null &&
|
||||
state
|
||||
.file!
|
||||
.isNotEmpty
|
||||
? state.file!
|
||||
: '${ApiUrls.baseUrl}${postCard!.pcImagePath}',
|
||||
pickImagesBloc:
|
||||
pickImagesBloc,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
SizedBox(height: 10.h),
|
||||
Text(
|
||||
"Edit message",
|
||||
style: GoogleFonts.poppins(
|
||||
color: Color(0XFF212121),
|
||||
fontSize: 18.sp,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 2.h),
|
||||
Text(
|
||||
"Edit your own unique postcards to cherish your unforgettable moments.",
|
||||
style: GoogleFonts.poppins(
|
||||
color: Color(0XFF000000).withValues(alpha: 0.6),
|
||||
fontSize: 14.sp,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 10.h),
|
||||
EditMessage(
|
||||
text: postCard!.pcContent,
|
||||
onChange: (message, font) {
|
||||
postCard = postCard!.copyWith(
|
||||
pcContent: getFormattedMessage(message, font),
|
||||
);
|
||||
},
|
||||
),
|
||||
SizedBox(height: 10.h),
|
||||
Form(
|
||||
key: _formKey,
|
||||
child: EditYourdetails(
|
||||
fullNameController: _fullNameController,
|
||||
addressController: _addressController,
|
||||
cityController: _cityController,
|
||||
zipCodeController: _zipCodeController,
|
||||
selectedCountry: _selectedCountry ?? "",
|
||||
selectedState: _selectedState ?? "",
|
||||
formKey: _formKey,
|
||||
selectState: (String p1) {
|
||||
setState(() {
|
||||
_selectedState = p1;
|
||||
});
|
||||
},
|
||||
selectCountry: (String p1) {
|
||||
setState(() {
|
||||
_selectedCountry = p1;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 30),
|
||||
|
||||
// Next Button
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
if (_formKey.currentState!.validate()) {
|
||||
postCard = postCard!.copyWith(
|
||||
fullname: _fullNameController.text,
|
||||
address1: _addressController.text,
|
||||
cityName: _cityController.text,
|
||||
zipCode: _zipCodeController.text,
|
||||
stateName: _selectedState,
|
||||
countryName: _selectedCountry,
|
||||
);
|
||||
editPostcardBloc.add(
|
||||
EditPostCard(
|
||||
myPostCard: postCard!,
|
||||
editImage: selectedImage,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xffF95F62),
|
||||
padding: EdgeInsets.symmetric(vertical: 16.h),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(40),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
"Next",
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 14.sp,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
Positioned(
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
child: state is EditPostcardSuccessfull
|
||||
? Center(
|
||||
child: SizedBox(
|
||||
width: 25,
|
||||
height: 25,
|
||||
child: CircularProgressIndicator(
|
||||
color: Color(0XFFF95F62),
|
||||
),
|
||||
),
|
||||
)
|
||||
: SizedBox(),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget imageButton({
|
||||
Function()? onPressed,
|
||||
required String title,
|
||||
IconData? icon,
|
||||
required double width,
|
||||
}) {
|
||||
return SizedBox(
|
||||
width: width,
|
||||
child: OutlinedButton(
|
||||
onPressed: onPressed,
|
||||
style: OutlinedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(vertical: 14, horizontal: 16),
|
||||
side: const BorderSide(color: Color(0xffF95F62)),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(30),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
color: Color(0xffF95F62),
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 12.sp,
|
||||
),
|
||||
),
|
||||
SizedBox(width: icon != null ? 8 : 0),
|
||||
icon != null ? Icon(icon, color: Color(0xffF95F62)) : SizedBox(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String getFormattedMessage(String message, String selectedFont) {
|
||||
if (message.isEmpty) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (selectedFont.isEmpty) {
|
||||
// Default font (Poppins)
|
||||
return '<span style="font-family: Poppins;">$message</span>';
|
||||
}
|
||||
|
||||
return '<span style="font-family: $selectedFont;">$message</span>';
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,12 @@
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:citycards_customer/postcard/blocs/edit_postcard/edit_postcard_bloc.dart';
|
||||
import 'package:citycards_customer/postcard/blocs/pick_images/pick_images_bloc.dart';
|
||||
import 'package:citycards_customer/postcard/views/edit_postcard_view.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:flutter_slidable/flutter_slidable.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import '../../core/route_constants.dart';
|
||||
@@ -10,9 +16,22 @@ import '../blocs/myPostCards/my_postcard_event.dart';
|
||||
import '../blocs/myPostCards/my_postcard_state.dart';
|
||||
import '../models/my_postcard_model.dart';
|
||||
|
||||
class MyPostCardDraftView extends StatelessWidget {
|
||||
class MyPostCardDraftView extends StatefulWidget {
|
||||
const MyPostCardDraftView({super.key});
|
||||
|
||||
@override
|
||||
State<MyPostCardDraftView> createState() => _MyPostCardDraftViewState();
|
||||
}
|
||||
|
||||
class _MyPostCardDraftViewState extends State<MyPostCardDraftView> {
|
||||
final TextEditingController _searchController = TextEditingController();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_searchController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<MyPostCardBloc, MyPostCardState>(
|
||||
@@ -22,14 +41,12 @@ class MyPostCardDraftView extends StatelessWidget {
|
||||
// Show loading indicator if drafts are loading
|
||||
if (state.isDraftLoading && state.draftPostCards.isEmpty) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(
|
||||
color: Color(0xffF95F62),
|
||||
),
|
||||
child: CircularProgressIndicator(color: Color(0xffF95F62)),
|
||||
);
|
||||
}
|
||||
|
||||
// Show empty state if no drafts
|
||||
if (state.draftPostCards.isEmpty) {
|
||||
if (state.allDraftPostCards.isEmpty) {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 24.w),
|
||||
@@ -79,19 +96,171 @@ class MyPostCardDraftView extends StatelessWidget {
|
||||
}
|
||||
|
||||
// Show the list of drafts
|
||||
return RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
context.read<MyPostCardBloc>().add(const RefreshDraftPostCards());
|
||||
},
|
||||
color: const Color(0xffF95F62),
|
||||
child: ListView.builder(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
itemCount: state.draftPostCards.length,
|
||||
itemBuilder: (context, index) {
|
||||
final postcard = state.draftPostCards[index];
|
||||
return _buildDraftCard(context, postcard);
|
||||
},
|
||||
),
|
||||
return Column(
|
||||
children: [
|
||||
// Search Field
|
||||
TextField(
|
||||
controller: _searchController,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Search drafts...',
|
||||
hintStyle: GoogleFonts.poppins(
|
||||
fontSize: 14.sp,
|
||||
color: Colors.black38,
|
||||
),
|
||||
suffixIcon: _searchController.text.isNotEmpty
|
||||
? IconButton(
|
||||
icon: Icon(
|
||||
Icons.clear,
|
||||
color: Colors.black38,
|
||||
size: 20,
|
||||
),
|
||||
onPressed: () {
|
||||
_searchController.clear();
|
||||
context.read<MyPostCardBloc>().add(
|
||||
const ClearDraftSearch(),
|
||||
);
|
||||
},
|
||||
)
|
||||
: null,
|
||||
filled: true,
|
||||
fillColor: Colors.white,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(
|
||||
color: Color(0xffF95F62).withOpacity(0.2),
|
||||
),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(
|
||||
color: Color(0xffF95F62).withOpacity(0.2),
|
||||
),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(
|
||||
color: Color(0xffF95F62),
|
||||
width: 1.5,
|
||||
),
|
||||
),
|
||||
contentPadding: EdgeInsets.symmetric(
|
||||
horizontal: 16.w,
|
||||
vertical: 12.h,
|
||||
),
|
||||
),
|
||||
onChanged: (query) {
|
||||
context.read<MyPostCardBloc>().add(
|
||||
SearchDraftPostCards(query: query),
|
||||
); // To update clear button visibility
|
||||
},
|
||||
),
|
||||
|
||||
// Search Results Info
|
||||
if (state.draftSearchQuery.isNotEmpty)
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16.w),
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
'Found ${state.draftPostCards.length} of ${state.allDraftPostCards.length} drafts',
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 12.sp,
|
||||
color: Colors.black54,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (state.draftSearchQuery.isNotEmpty) SizedBox(height: 8.h),
|
||||
|
||||
// List with Stack for loading
|
||||
Expanded(
|
||||
child: Stack(
|
||||
children: [
|
||||
RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
context.read<MyPostCardBloc>().add(
|
||||
const RefreshDraftPostCards(),
|
||||
);
|
||||
},
|
||||
color: const Color(0xffF95F62),
|
||||
child: state.draftPostCards.isEmpty
|
||||
? ListView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
children: [
|
||||
SizedBox(height: 100.h),
|
||||
Center(
|
||||
child: Column(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.search_off,
|
||||
size: 48,
|
||||
color: Colors.black26,
|
||||
),
|
||||
SizedBox(height: 16.h),
|
||||
Text(
|
||||
'No search available',
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 16.sp,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.black54,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 8.h),
|
||||
if (state.draftSearchQuery.isNotEmpty)
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 32.w,
|
||||
),
|
||||
child: Text(
|
||||
'Try searching with different keywords',
|
||||
textAlign: TextAlign.center,
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 14.sp,
|
||||
color: Colors.black38,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
: ListView.builder(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 12.w,
|
||||
vertical: 12.h,
|
||||
),
|
||||
itemCount: state.draftPostCards.length,
|
||||
itemBuilder: (context, index) {
|
||||
final postcard = state.draftPostCards[index];
|
||||
return _buildDraftCard(context, postcard);
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
Positioned(
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
child: state.isDeleteLoading == true
|
||||
? Center(
|
||||
child: SizedBox(
|
||||
width: 25,
|
||||
height: 25,
|
||||
child: CircularProgressIndicator(
|
||||
color: Color(0XFFF95F62),
|
||||
),
|
||||
),
|
||||
)
|
||||
: SizedBox(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -130,11 +299,16 @@ class MyPostCardDraftView extends StatelessWidget {
|
||||
const SizedBox(height: 24),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
context.read<MyPostCardBloc>().add(const FetchDraftPostCards());
|
||||
context.read<MyPostCardBloc>().add(
|
||||
const FetchDraftPostCards(),
|
||||
);
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xffF95F62),
|
||||
padding: EdgeInsets.symmetric(horizontal: 32.w, vertical: 12.h),
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 32.w,
|
||||
vertical: 12.h,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(40),
|
||||
),
|
||||
@@ -160,131 +334,221 @@ class MyPostCardDraftView extends StatelessWidget {
|
||||
|
||||
Widget _buildDraftCard(BuildContext context, MyPostCard postcard) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 16),
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xffF95F62).withValues(alpha: 0.08),
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
border: Border.all(color: const Color(0xffF1F5F7)),
|
||||
color: const Color(0xffF95F62),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border(left: BorderSide(width: 6, color: Color(0XFFF93232))),
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
/// LEFT IMAGE
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
child: Image.network(
|
||||
'${ApiUrls.baseUrl}${postcard.pcImagePath}',
|
||||
height: 72,
|
||||
width: 72,
|
||||
fit: BoxFit.cover,
|
||||
loadingBuilder: (context, child, loadingProgress) {
|
||||
if (loadingProgress == null) return child;
|
||||
return Container(
|
||||
height: 72,
|
||||
width: 72,
|
||||
color: Colors.grey[300],
|
||||
child: const Center(
|
||||
child: CircularProgressIndicator(
|
||||
color: Color(0xffF95F62),
|
||||
strokeWidth: 2,
|
||||
margin: EdgeInsets.only(bottom: 15),
|
||||
child: Slidable(
|
||||
key: UniqueKey(),
|
||||
startActionPane: ActionPane(
|
||||
motion: const ScrollMotion(),
|
||||
|
||||
// dismissible: DismissiblePane(onDismissed: () {}),
|
||||
children: [
|
||||
SlidableAction(
|
||||
onPressed: (ctx) {
|
||||
context.read<MyPostCardBloc>().add(
|
||||
DeleteDraftPostCards(id: postcard.id),
|
||||
);
|
||||
},
|
||||
flex: 3,
|
||||
backgroundColor: Color(0XFFF93232),
|
||||
foregroundColor: Colors.white,
|
||||
icon: Icons.delete,
|
||||
label: 'Delete',
|
||||
autoClose: true,
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(10),
|
||||
bottomLeft: Radius.circular(10),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(color: const Color(0XFFFFF5F5)),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
/// NUMBER
|
||||
Text(
|
||||
"#${postcard.pcNumber}",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.black.withValues(alpha: 0.4),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
/// LEFT IMAGE
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
child: Image.network(
|
||||
'${ApiUrls.baseUrl}${postcard.pcImagePath}',
|
||||
height: 72,
|
||||
width: 72,
|
||||
fit: BoxFit.cover,
|
||||
loadingBuilder: (context, child, loadingProgress) {
|
||||
if (loadingProgress == null) return child;
|
||||
return Container(
|
||||
height: 72,
|
||||
width: 72,
|
||||
color: Colors.grey[300],
|
||||
child: const Center(
|
||||
child: CircularProgressIndicator(
|
||||
color: Color(0xffF95F62),
|
||||
strokeWidth: 2,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
return Container(
|
||||
height: 72,
|
||||
width: 72,
|
||||
color: Colors.grey[300],
|
||||
child: const Icon(
|
||||
Icons.image_not_supported,
|
||||
color: Colors.grey,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
return Container(
|
||||
height: 72,
|
||||
width: 72,
|
||||
color: Colors.grey[300],
|
||||
child: const Icon(
|
||||
Icons.image_not_supported,
|
||||
color: Colors.grey,
|
||||
|
||||
const SizedBox(width: 14),
|
||||
|
||||
/// RIGHT CONTENT
|
||||
Expanded(
|
||||
child: Text(
|
||||
postcard.pcTitle,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
onPressed: () async {
|
||||
final result = await Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (_) => MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider(
|
||||
create: (context) => EditPostcardBloc(),
|
||||
),
|
||||
BlocProvider(
|
||||
create: (context) => PickImagesBloc(),
|
||||
),
|
||||
],
|
||||
|
||||
child: EditPostcardView(myPostCard: postcard),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
if (result == true) {
|
||||
// ignore: use_build_context_synchronously
|
||||
context.read<MyPostCardBloc>().add(
|
||||
const RefreshDraftPostCards(),
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Color(
|
||||
0xfff95f62,
|
||||
).withValues(alpha: 0.1),
|
||||
elevation: 0,
|
||||
padding: EdgeInsets.symmetric(vertical: 16.h),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
side: BorderSide(color: Color(0XFFFDCDCE)),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.edit_outlined,
|
||||
size: 22,
|
||||
color: Color(0XFFF95F62),
|
||||
),
|
||||
SizedBox(width: 5),
|
||||
Text(
|
||||
"Edit",
|
||||
style: GoogleFonts.poppins(
|
||||
color: Color(0XFFF95F62),
|
||||
fontSize: 14.sp,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 4),
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
onPressed: () {},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Color(
|
||||
0xfff95f62,
|
||||
).withValues(alpha: 0.1),
|
||||
elevation: 0,
|
||||
padding: EdgeInsets.symmetric(vertical: 16.h),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
side: BorderSide(color: Color(0XFFFDCDCE)),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Transform.rotate(
|
||||
angle: -45,
|
||||
child: Icon(
|
||||
Icons.send_outlined,
|
||||
size: 22,
|
||||
color: Color(0XFFF95F62),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 5),
|
||||
Text(
|
||||
"Send",
|
||||
style: GoogleFonts.poppins(
|
||||
color: Color(0XFFF95F62),
|
||||
fontSize: 14.sp,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(width: 14),
|
||||
|
||||
/// RIGHT CONTENT
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
/// NUMBER
|
||||
Text(
|
||||
"#${postcard.pcNumber}",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.black,
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 4),
|
||||
|
||||
/// TITLE
|
||||
Text(
|
||||
postcard.pcTitle,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 10),
|
||||
|
||||
/// ICONS – BOTTOM RIGHT (UNDER TITLE)
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
// delete
|
||||
},
|
||||
child: Image.asset(
|
||||
'assets/icons/delete_icon.png',
|
||||
width: 20,
|
||||
height: 20,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
// edit
|
||||
},
|
||||
child: Image.asset(
|
||||
'assets/icons/edit_icon.png',
|
||||
width: 20,
|
||||
height: 20,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
// send
|
||||
},
|
||||
child: Image.asset(
|
||||
'assets/icons/send_icon.png',
|
||||
width: 20,
|
||||
height: 20,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,9 +11,22 @@ import '../models/my_postcard_model.dart';
|
||||
import '../../networkApiServices/api_urls.dart';
|
||||
import 'my_postcard_preview_view.dart';
|
||||
|
||||
class MyPostCardOrdersView extends StatelessWidget {
|
||||
class MyPostCardOrdersView extends StatefulWidget {
|
||||
const MyPostCardOrdersView({super.key});
|
||||
|
||||
@override
|
||||
State<MyPostCardOrdersView> createState() => _MyPostCardOrdersViewState();
|
||||
}
|
||||
|
||||
class _MyPostCardOrdersViewState extends State<MyPostCardOrdersView> {
|
||||
final TextEditingController _searchController = TextEditingController();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_searchController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<MyPostCardBloc, MyPostCardState>(
|
||||
@@ -23,14 +36,12 @@ class MyPostCardOrdersView extends StatelessWidget {
|
||||
// Show loading indicator if orders are loading
|
||||
if (state.isOrderLoading && state.orderPostCards.isEmpty) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(
|
||||
color: Color(0xffF95F62),
|
||||
),
|
||||
child: CircularProgressIndicator(color: Color(0xffF95F62)),
|
||||
);
|
||||
}
|
||||
|
||||
// Show empty state if no orders
|
||||
if (state.orderPostCards.isEmpty) {
|
||||
if (state.allOrderPostCards.isEmpty) {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 24.w),
|
||||
@@ -79,20 +90,162 @@ class MyPostCardOrdersView extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
// Show the list of orders
|
||||
return RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
context.read<MyPostCardBloc>().add(const RefreshOrderPostCards());
|
||||
},
|
||||
color: const Color(0xffF95F62),
|
||||
child: ListView.builder(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
itemCount: state.orderPostCards.length,
|
||||
itemBuilder: (context, index) {
|
||||
final postcard = state.orderPostCards[index];
|
||||
return _buildOrderCard(context, postcard);
|
||||
},
|
||||
),
|
||||
// Show the list of orders with search
|
||||
return Column(
|
||||
children: [
|
||||
// Search Field
|
||||
TextField(
|
||||
controller: _searchController,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Search orders...',
|
||||
hintStyle: GoogleFonts.poppins(
|
||||
fontSize: 14.sp,
|
||||
color: Colors.black38,
|
||||
),
|
||||
suffixIcon: _searchController.text.isNotEmpty
|
||||
? IconButton(
|
||||
icon: Icon(
|
||||
Icons.clear,
|
||||
color: Colors.black38,
|
||||
size: 20,
|
||||
),
|
||||
onPressed: () {
|
||||
_searchController.clear();
|
||||
context.read<MyPostCardBloc>().add(
|
||||
const ClearOrderSearch(),
|
||||
);
|
||||
},
|
||||
)
|
||||
: null,
|
||||
filled: true,
|
||||
fillColor: Colors.white,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(
|
||||
color: Color(0xffF95F62).withOpacity(0.2),
|
||||
),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(
|
||||
color: Color(0xffF95F62).withOpacity(0.2),
|
||||
),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(
|
||||
color: Color(0xffF95F62),
|
||||
width: 1.5,
|
||||
),
|
||||
),
|
||||
contentPadding: EdgeInsets.symmetric(
|
||||
horizontal: 16.w,
|
||||
vertical: 12.h,
|
||||
),
|
||||
),
|
||||
onChanged: (query) {
|
||||
context.read<MyPostCardBloc>().add(
|
||||
SearchOrderPostCards(query: query),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
// Search Results Info
|
||||
if (state.orderSearchQuery.isNotEmpty)
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16.w),
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
'Found ${state.orderPostCards.length} of ${state.allOrderPostCards.length} orders',
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 12.sp,
|
||||
color: Colors.black54,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (state.orderSearchQuery.isNotEmpty) SizedBox(height: 8.h),
|
||||
|
||||
// List with Stack for loading
|
||||
Expanded(
|
||||
child: Stack(
|
||||
children: [
|
||||
RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
context.read<MyPostCardBloc>().add(
|
||||
const RefreshOrderPostCards(),
|
||||
);
|
||||
},
|
||||
color: const Color(0xffF95F62),
|
||||
child: state.orderPostCards.isEmpty
|
||||
? ListView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
children: [
|
||||
SizedBox(height: 100.h),
|
||||
Center(
|
||||
child: Column(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.search_off,
|
||||
size: 48,
|
||||
color: Colors.black26,
|
||||
),
|
||||
SizedBox(height: 16.h),
|
||||
Text(
|
||||
'No orders found',
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 16.sp,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.black54,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 8.h),
|
||||
if (state.orderSearchQuery.isNotEmpty)
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 32.w,
|
||||
),
|
||||
child: Text(
|
||||
'Try adjusting your search query',
|
||||
textAlign: TextAlign.center,
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 14.sp,
|
||||
color: Colors.black38,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
: ListView.builder(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
itemCount: state.orderPostCards.length,
|
||||
padding: EdgeInsets.only(top: 16.h),
|
||||
itemBuilder: (context, index) {
|
||||
final postcard = state.orderPostCards[index];
|
||||
return _buildOrderCard(context, postcard);
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
// Loading overlay
|
||||
if (state.isOrderLoading)
|
||||
Container(
|
||||
color: Colors.white.withOpacity(0.7),
|
||||
child: const Center(
|
||||
child: CircularProgressIndicator(
|
||||
color: Color(0xffF95F62),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -131,11 +284,16 @@ class MyPostCardOrdersView extends StatelessWidget {
|
||||
const SizedBox(height: 24),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
context.read<MyPostCardBloc>().add(const FetchOrderPostCards());
|
||||
context.read<MyPostCardBloc>().add(
|
||||
const FetchOrderPostCards(),
|
||||
);
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xffF95F62),
|
||||
padding: EdgeInsets.symmetric(horizontal: 32.w, vertical: 12.h),
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 32.w,
|
||||
vertical: 12.h,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(40),
|
||||
),
|
||||
@@ -160,41 +318,71 @@ class MyPostCardOrdersView extends StatelessWidget {
|
||||
}
|
||||
|
||||
Widget _buildOrderCard(BuildContext context, MyPostCard postcard) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Postcard Number above the card
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 4, bottom: 8),
|
||||
child: Text(
|
||||
"#${postcard.pcNumber}",
|
||||
style: GoogleFonts.poppins(
|
||||
color: Colors.black,
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 15.sp,
|
||||
),
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 16),
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xffF95F62).withValues(alpha: 0.08),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: const Color(0xffF1F5F7)),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
"#${postcard.pcNumber}",
|
||||
style: GoogleFonts.poppins(
|
||||
color: Colors.black.withValues(alpha: 0.4),
|
||||
fontWeight: FontWeight.w400,
|
||||
fontSize: 12.sp,
|
||||
),
|
||||
),
|
||||
SizedBox(width: 10),
|
||||
Spacer(),
|
||||
Text(
|
||||
"Status:",
|
||||
style: GoogleFonts.poppins(
|
||||
color: Colors.black,
|
||||
fontWeight: FontWeight.w400,
|
||||
fontSize: 10.sp,
|
||||
),
|
||||
),
|
||||
SizedBox(width: 5),
|
||||
Container(
|
||||
padding: const EdgeInsets.fromLTRB(13, 7, 13, 7),
|
||||
decoration: BoxDecoration(
|
||||
color: _getStatusColor(
|
||||
postcard.orderStatus,
|
||||
).withOpacity(0.16),
|
||||
border: Border.all(
|
||||
color: _getStatusBorderColor(postcard.orderStatus),
|
||||
),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Text(
|
||||
_getStatusText(postcard.orderStatus),
|
||||
style: TextStyle(
|
||||
color: Colors.black,
|
||||
fontWeight: FontWeight.w400,
|
||||
fontSize: 8.54.sp,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Order Card
|
||||
Container(
|
||||
margin: const EdgeInsets.only(bottom: 16),
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xffF95F62).withValues(alpha:0.08),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: const Color(0xffF1F5F7),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
SizedBox(width: 10),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
// Postcard Image
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Image(
|
||||
image: NetworkImage('${ApiUrls.baseUrl}${postcard.pcImagePath}'),
|
||||
image: NetworkImage(
|
||||
'${ApiUrls.baseUrl}${postcard.pcImagePath}',
|
||||
),
|
||||
height: 70.h,
|
||||
width: 70.w,
|
||||
fit: BoxFit.cover,
|
||||
@@ -233,98 +421,67 @@ class MyPostCardOrdersView extends StatelessWidget {
|
||||
|
||||
// Postcard Details
|
||||
Expanded(
|
||||
child: SizedBox(
|
||||
child: Container(
|
||||
alignment: Alignment.centerLeft,
|
||||
height: 60.h,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
postcard.pcTitle,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: GoogleFonts.poppins(
|
||||
color: Colors.black,
|
||||
fontWeight: FontWeight.w400,
|
||||
fontSize: 16.sp,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
"5 Post cards",
|
||||
style: GoogleFonts.poppins(
|
||||
color: Colors.black,
|
||||
fontWeight: FontWeight.w400,
|
||||
fontSize: 14.sp,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.fromLTRB(13, 7, 13, 7),
|
||||
decoration: BoxDecoration(
|
||||
color: _getStatusColor(postcard.orderStatus).withOpacity(0.16),
|
||||
border: Border.all(
|
||||
color: _getStatusBorderColor(postcard.orderStatus),
|
||||
),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Text(
|
||||
_getStatusText(postcard.orderStatus),
|
||||
style: TextStyle(
|
||||
color: Colors.black,
|
||||
fontWeight: FontWeight.w400,
|
||||
fontSize: 8.54.sp,
|
||||
),
|
||||
),
|
||||
),
|
||||
InkWell(
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => MyPostcardPreviewView(
|
||||
postcard: postcard,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.remove_red_eye_outlined,
|
||||
size: 15,
|
||||
color: const Color(0xffF95F62),
|
||||
),
|
||||
SizedBox(width: 5.w),
|
||||
Text(
|
||||
"Preview",
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w400,
|
||||
color: const Color(0xffF95F62),
|
||||
fontSize: 13.sp,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
child: Text(
|
||||
postcard.pcTitle,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: GoogleFonts.poppins(
|
||||
color: Colors.black,
|
||||
fontWeight: FontWeight.w400,
|
||||
fontSize: 16.sp,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
Container(
|
||||
margin: EdgeInsets.only(top: 10),
|
||||
child: ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Color(0xfff95f62).withValues(alpha: 0.1),
|
||||
elevation: 0,
|
||||
padding: EdgeInsets.symmetric(vertical: 16.h),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
side: BorderSide(color: Color(0XFFFDCDCE)),
|
||||
),
|
||||
onPressed: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) =>
|
||||
MyPostcardPreviewView(postcard: postcard),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.remove_red_eye_outlined,
|
||||
size: 15,
|
||||
color: const Color(0xffF95F62),
|
||||
),
|
||||
SizedBox(width: 5.w),
|
||||
Text(
|
||||
"Preview",
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w400,
|
||||
color: const Color(0xffF95F62),
|
||||
fontSize: 13.sp,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,465 +1,484 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import '../../common_packages/app_bar.dart';
|
||||
import '../../common_packages/back_widget.dart';
|
||||
import '../models/my_postcard_model.dart';
|
||||
import '../../networkApiServices/api_urls.dart';
|
||||
import '../widgets/back_card_widget.dart';
|
||||
import '../widgets/front_card_widget.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import '../../common_packages/app_bar.dart';
|
||||
import '../../common_packages/back_widget.dart';
|
||||
import '../blocs/edit_postcard/edit_postcard_bloc.dart';
|
||||
import '../blocs/myPostCards/my_postcard_bloc.dart';
|
||||
import '../blocs/myPostCards/my_postcard_event.dart';
|
||||
import '../models/my_postcard_model.dart';
|
||||
import '../../networkApiServices/api_urls.dart';
|
||||
import '../widgets/back_card_widget.dart';
|
||||
import '../widgets/front_card_widget.dart';
|
||||
import 'edit_postcard_view.dart';
|
||||
|
||||
class MyPostcardPreviewView extends StatefulWidget {
|
||||
final MyPostCard postcard;
|
||||
class MyPostcardPreviewView extends StatefulWidget {
|
||||
final MyPostCard postcard;
|
||||
|
||||
const MyPostcardPreviewView({
|
||||
super.key,
|
||||
required this.postcard,
|
||||
});
|
||||
const MyPostcardPreviewView({
|
||||
super.key,
|
||||
required this.postcard,
|
||||
});
|
||||
|
||||
@override
|
||||
State<MyPostcardPreviewView> createState() => _MyPostcardPreviewViewState();
|
||||
}
|
||||
@override
|
||||
State<MyPostcardPreviewView> createState() => _MyPostcardPreviewViewState();
|
||||
}
|
||||
|
||||
class _MyPostcardPreviewViewState extends State<MyPostcardPreviewView> {
|
||||
bool showBack = false;
|
||||
class _MyPostcardPreviewViewState extends State<MyPostcardPreviewView> {
|
||||
bool showBack = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
body: SafeArea(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 10.h),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
CommonAppBar(
|
||||
isWhiteLogo: false,
|
||||
isProfilePage: false,
|
||||
showDivider: true,
|
||||
),
|
||||
backWidget(context, "Preview", Colors.black),
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
body: SafeArea(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 10.h),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
CommonAppBar(
|
||||
isWhiteLogo: false,
|
||||
isProfilePage: false,
|
||||
showDivider: true,
|
||||
),
|
||||
backWidget(context, "Preview", Colors.black),
|
||||
|
||||
SizedBox(height: 29.h),
|
||||
// Postcard Number with Action Icons
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 20.w),
|
||||
child: Row(
|
||||
children: [
|
||||
/// PC Number (takes only available space)
|
||||
Expanded(
|
||||
child: Text(
|
||||
widget.postcard.pcNumber,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: GoogleFonts.poppins(
|
||||
color: Colors.black,
|
||||
fontSize: 18.sp,
|
||||
fontWeight: FontWeight.w400,
|
||||
SizedBox(height: 29.h),
|
||||
// Postcard Number with Action Icons
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 20.w),
|
||||
child: Row(
|
||||
children: [
|
||||
/// PC Number (takes only available space)
|
||||
Expanded(
|
||||
child: Text(
|
||||
widget.postcard.pcNumber,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: GoogleFonts.poppins(
|
||||
color: Colors.black,
|
||||
fontSize: 18.sp,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
SizedBox(width: 12.w),
|
||||
SizedBox(width: 12.w),
|
||||
|
||||
/// Action Icons
|
||||
Row(
|
||||
/// Action Icons
|
||||
Row(
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
// Delete functionality
|
||||
},
|
||||
child: Image.asset(
|
||||
'assets/icons/delete_icon.png',
|
||||
width: 24,
|
||||
height: 24,
|
||||
),
|
||||
),
|
||||
SizedBox(width: 16.w),
|
||||
GestureDetector(
|
||||
onTap: () async {
|
||||
final result = await Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (_) => BlocProvider(
|
||||
create: (context) => EditPostcardBloc(),
|
||||
child: EditPostcardView(myPostCard: widget.postcard),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
if (result == true) {
|
||||
// ignore: use_build_context_synchronously
|
||||
context.read<MyPostCardBloc>().add(
|
||||
const RefreshOrderPostCards(),
|
||||
);
|
||||
}
|
||||
},
|
||||
child: Image.asset(
|
||||
'assets/icons/edit_icon.png',
|
||||
width: 24,
|
||||
height: 24,
|
||||
),
|
||||
),
|
||||
SizedBox(width: 16.w),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
// Send functionality
|
||||
},
|
||||
child: Image.asset(
|
||||
'assets/icons/send_icon.png',
|
||||
width: 24,
|
||||
height: 24,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(height: 20.h),
|
||||
|
||||
// Flip buttons
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 20.w),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
// Delete functionality
|
||||
setState(() {
|
||||
showBack = false;
|
||||
});
|
||||
},
|
||||
child: Image.asset(
|
||||
'assets/icons/delete_icon.png',
|
||||
width: 24,
|
||||
height: 24,
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.arrow_back,
|
||||
color: !showBack ? Colors.grey[400] : const Color(0xffF95F62),
|
||||
size: 20,
|
||||
),
|
||||
SizedBox(width: 6.w),
|
||||
Text(
|
||||
'Flip',
|
||||
style: GoogleFonts.poppins(
|
||||
color: !showBack ? Colors.grey[400] : const Color(0xffF95F62),
|
||||
fontSize: 14.sp,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(width: 16.w),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
// Edit functionality
|
||||
setState(() {
|
||||
showBack = true;
|
||||
});
|
||||
},
|
||||
child: Image.asset(
|
||||
'assets/icons/edit_icon.png',
|
||||
width: 24,
|
||||
height: 24,
|
||||
),
|
||||
),
|
||||
SizedBox(width: 16.w),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
// Send functionality
|
||||
},
|
||||
child: Image.asset(
|
||||
'assets/icons/send_icon.png',
|
||||
width: 24,
|
||||
height: 24,
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
'Flip',
|
||||
style: GoogleFonts.poppins(
|
||||
color: showBack ? Colors.grey[400] : const Color(0xffF95F62),
|
||||
fontSize: 14.sp,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
SizedBox(width: 6.w),
|
||||
Icon(
|
||||
Icons.arrow_forward,
|
||||
color: showBack ? Colors.grey[400] : const Color(0xffF95F62),
|
||||
size: 20,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(height: 20.h),
|
||||
|
||||
// Flip buttons
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 20.w),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
setState(() {
|
||||
showBack = false;
|
||||
});
|
||||
},
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.arrow_back,
|
||||
color: !showBack ? Colors.grey[400] : const Color(0xffF95F62),
|
||||
size: 20,
|
||||
),
|
||||
SizedBox(width: 6.w),
|
||||
Text(
|
||||
'Flip',
|
||||
style: GoogleFonts.poppins(
|
||||
color: !showBack ? Colors.grey[400] : const Color(0xffF95F62),
|
||||
fontSize: 14.sp,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
setState(() {
|
||||
showBack = true;
|
||||
});
|
||||
},
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
'Flip',
|
||||
style: GoogleFonts.poppins(
|
||||
color: showBack ? Colors.grey[400] : const Color(0xffF95F62),
|
||||
fontSize: 14.sp,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
SizedBox(width: 6.w),
|
||||
Icon(
|
||||
Icons.arrow_forward,
|
||||
color: showBack ? Colors.grey[400] : const Color(0xffF95F62),
|
||||
size: 20,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(top: 40.h),
|
||||
child: Align(
|
||||
alignment: Alignment.topCenter,
|
||||
child: AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 400),
|
||||
transitionBuilder: (child, animation) {
|
||||
return FadeTransition(
|
||||
opacity: animation,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
child: showBack
|
||||
? BackCardWidget(
|
||||
key: const ValueKey('back'),
|
||||
message: widget.postcard.pcContent,
|
||||
city: widget.postcard.cityName,
|
||||
state: widget.postcard.stateName,
|
||||
country: widget.postcard.countryName,
|
||||
address: widget.postcard.address1,
|
||||
name: widget.postcard.fullname,
|
||||
pincode: widget.postcard.zipCode,
|
||||
)
|
||||
: FrontCardWidget(
|
||||
key: const ValueKey('front'),
|
||||
imageUrl:
|
||||
'${ApiUrls.baseUrl}${widget.postcard.pcImagePath}',
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(top: 40.h),
|
||||
child: Align(
|
||||
alignment: Alignment.topCenter,
|
||||
child: AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 400),
|
||||
transitionBuilder: (child, animation) {
|
||||
return FadeTransition(
|
||||
opacity: animation,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
child: showBack
|
||||
? BackCardWidget(
|
||||
key: const ValueKey('back'),
|
||||
message: widget.postcard.pcContent,
|
||||
city: widget.postcard.cityName,
|
||||
state: widget.postcard.stateName,
|
||||
country: widget.postcard.countryName,
|
||||
address: widget.postcard.address1,
|
||||
name: widget.postcard.fullname,
|
||||
pincode: widget.postcard.zipCode,
|
||||
)
|
||||
: FrontCardWidget(
|
||||
key: const ValueKey('front'),
|
||||
imageUrl:
|
||||
'${ApiUrls.baseUrl}${widget.postcard.pcImagePath}',
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
// Expanded(
|
||||
// child: Padding(
|
||||
// padding: EdgeInsets.only(top: 40.h),
|
||||
// child: AnimatedSwitcher(
|
||||
// duration: const Duration(milliseconds: 400),
|
||||
// transitionBuilder: (Widget child, Animation<double> animation) {
|
||||
// return FadeTransition(
|
||||
// opacity: animation,
|
||||
// child: child,
|
||||
// );
|
||||
// },
|
||||
// child: showBack ? _buildBackSide() : _buildFrontSide(),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
SizedBox(height: 40.h),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFrontSide() {
|
||||
return Container(
|
||||
key: const ValueKey('front'),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: AspectRatio(
|
||||
aspectRatio: 1.5, // Standard postcard ratio
|
||||
child: Image.network(
|
||||
'${ApiUrls.baseUrl}${widget.postcard.pcImagePath}',
|
||||
fit: BoxFit.cover,
|
||||
loadingBuilder: (context, child, loadingProgress) {
|
||||
if (loadingProgress == null) return child;
|
||||
return Container(
|
||||
color: Colors.grey[300],
|
||||
child: Center(
|
||||
child: CircularProgressIndicator(
|
||||
color: const Color(0xffF95F62),
|
||||
value: loadingProgress.expectedTotalBytes != null
|
||||
? loadingProgress.cumulativeBytesLoaded /
|
||||
loadingProgress.expectedTotalBytes!
|
||||
: null,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
return Container(
|
||||
color: Colors.grey[300],
|
||||
child: const Center(
|
||||
child: Icon(
|
||||
Icons.image_not_supported,
|
||||
size: 60,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBackSide() {
|
||||
return Container(
|
||||
key: const ValueKey('back'),
|
||||
decoration: BoxDecoration(
|
||||
gradient: const LinearGradient(
|
||||
colors: [
|
||||
Color(0xffE2D6C2),
|
||||
Color(0xffFFF5E6),
|
||||
Color(0xffFFF5E6),
|
||||
],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
border: Border.all(
|
||||
color: const Color(0xff000000).withOpacity(0.12),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: AspectRatio(
|
||||
aspectRatio: 1.5,
|
||||
child: Row(
|
||||
children: [
|
||||
// ================= LEFT SIDE =================
|
||||
Expanded(
|
||||
flex: 55,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 14.h),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Logo
|
||||
Image.asset(
|
||||
'assets/logo/logo_city_cards.png',
|
||||
height: 24.h, // adjust as needed
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
SizedBox(height: 2.h),
|
||||
Text(
|
||||
'POSTCARD',
|
||||
style: TextStyle(
|
||||
color: Colors.black45,
|
||||
fontSize: 6.sp,
|
||||
letterSpacing: 1.4,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
|
||||
SizedBox(height: 14.h),
|
||||
|
||||
// Message label
|
||||
Text(
|
||||
'MESSAGE PREVIEW',
|
||||
style: TextStyle(
|
||||
color: Colors.black,
|
||||
fontSize: 6.sp,
|
||||
letterSpacing: 1.4,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 8.h),
|
||||
|
||||
// Message text
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
child: Text(
|
||||
widget.postcard.pcContent,
|
||||
style: TextStyle(
|
||||
color: Colors.black87,
|
||||
fontSize: 13.sp,
|
||||
height: 1.45,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
SizedBox(height: 10.h),
|
||||
|
||||
// Footer
|
||||
Text(
|
||||
'CityCards.co',
|
||||
style: TextStyle(
|
||||
color: const Color(0xffF95F62),
|
||||
fontSize: 12.sp,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// ================= DIVIDER =================
|
||||
Container(
|
||||
width: 4,
|
||||
margin: EdgeInsets.symmetric(vertical: 14.h),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
Colors.black.withOpacity(0.05),
|
||||
Colors.black.withOpacity(0.30),
|
||||
Colors.black.withOpacity(0.05),
|
||||
],
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// ================= RIGHT SIDE =================
|
||||
Expanded(
|
||||
flex: 45,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 14.w, vertical: 14.h),
|
||||
child: Column(
|
||||
children: [
|
||||
const Spacer(),
|
||||
|
||||
// Address with BORDER
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: EdgeInsets.all(4.w),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
border: Border.all(
|
||||
color: Colors.black.withOpacity(0.15),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
// ADDRESS label
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
'ADDRESS',
|
||||
style: TextStyle(
|
||||
color: Colors.black45,
|
||||
fontSize: 7.5.sp,
|
||||
letterSpacing: 1.6,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
SizedBox(height: 6.h),
|
||||
|
||||
// Address line 1
|
||||
Text(
|
||||
'${widget.postcard.cityName},',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: Colors.black87,
|
||||
fontSize: 13.sp,
|
||||
height: 1.5,
|
||||
),
|
||||
),
|
||||
|
||||
SizedBox(height: 6.h),
|
||||
|
||||
// State
|
||||
Text(
|
||||
'${widget.postcard.stateName},',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: Colors.black87,
|
||||
fontSize: 13.sp,
|
||||
height: 1.5,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 6.h),
|
||||
// Country
|
||||
Text(
|
||||
widget.postcard.countryName,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: Colors.black87,
|
||||
fontSize: 13.sp,
|
||||
height: 1.5,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const Spacer(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
// Expanded(
|
||||
// child: Padding(
|
||||
// padding: EdgeInsets.only(top: 40.h),
|
||||
// child: AnimatedSwitcher(
|
||||
// duration: const Duration(milliseconds: 400),
|
||||
// transitionBuilder: (Widget child, Animation<double> animation) {
|
||||
// return FadeTransition(
|
||||
// opacity: animation,
|
||||
// child: child,
|
||||
// );
|
||||
// },
|
||||
// child: showBack ? _buildBackSide() : _buildFrontSide(),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
SizedBox(height: 40.h),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFrontSide() {
|
||||
return Container(
|
||||
key: const ValueKey('front'),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: AspectRatio(
|
||||
aspectRatio: 1.5, // Standard postcard ratio
|
||||
child: Image.network(
|
||||
'${ApiUrls.baseUrl}${widget.postcard.pcImagePath}',
|
||||
fit: BoxFit.cover,
|
||||
loadingBuilder: (context, child, loadingProgress) {
|
||||
if (loadingProgress == null) return child;
|
||||
return Container(
|
||||
color: Colors.grey[300],
|
||||
child: Center(
|
||||
child: CircularProgressIndicator(
|
||||
color: const Color(0xffF95F62),
|
||||
value: loadingProgress.expectedTotalBytes != null
|
||||
? loadingProgress.cumulativeBytesLoaded /
|
||||
loadingProgress.expectedTotalBytes!
|
||||
: null,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
return Container(
|
||||
color: Colors.grey[300],
|
||||
child: const Center(
|
||||
child: Icon(
|
||||
Icons.image_not_supported,
|
||||
size: 60,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBackSide() {
|
||||
return Container(
|
||||
key: const ValueKey('back'),
|
||||
decoration: BoxDecoration(
|
||||
gradient: const LinearGradient(
|
||||
colors: [
|
||||
Color(0xffE2D6C2),
|
||||
Color(0xffFFF5E6),
|
||||
Color(0xffFFF5E6),
|
||||
],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
border: Border.all(
|
||||
color: const Color(0xff000000).withOpacity(0.12),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: AspectRatio(
|
||||
aspectRatio: 1.5,
|
||||
child: Row(
|
||||
children: [
|
||||
// ================= LEFT SIDE =================
|
||||
Expanded(
|
||||
flex: 55,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 14.h),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Logo
|
||||
Image.asset(
|
||||
'assets/logo/logo_city_cards.png',
|
||||
height: 24.h, // adjust as needed
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
SizedBox(height: 2.h),
|
||||
Text(
|
||||
'POSTCARD',
|
||||
style: TextStyle(
|
||||
color: Colors.black45,
|
||||
fontSize: 6.sp,
|
||||
letterSpacing: 1.4,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
|
||||
SizedBox(height: 14.h),
|
||||
|
||||
// Message label
|
||||
Text(
|
||||
'MESSAGE PREVIEW',
|
||||
style: TextStyle(
|
||||
color: Colors.black,
|
||||
fontSize: 6.sp,
|
||||
letterSpacing: 1.4,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 8.h),
|
||||
|
||||
// Message text
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
child: Text(
|
||||
widget.postcard.pcContent,
|
||||
style: TextStyle(
|
||||
color: Colors.black87,
|
||||
fontSize: 13.sp,
|
||||
height: 1.45,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
SizedBox(height: 10.h),
|
||||
|
||||
// Footer
|
||||
Text(
|
||||
'CityCards.co',
|
||||
style: TextStyle(
|
||||
color: const Color(0xffF95F62),
|
||||
fontSize: 12.sp,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// ================= DIVIDER =================
|
||||
Container(
|
||||
width: 4,
|
||||
margin: EdgeInsets.symmetric(vertical: 14.h),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
Colors.black.withOpacity(0.05),
|
||||
Colors.black.withOpacity(0.30),
|
||||
Colors.black.withOpacity(0.05),
|
||||
],
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// ================= RIGHT SIDE =================
|
||||
Expanded(
|
||||
flex: 45,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 14.w, vertical: 14.h),
|
||||
child: Column(
|
||||
children: [
|
||||
const Spacer(),
|
||||
|
||||
// Address with BORDER
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: EdgeInsets.all(4.w),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
border: Border.all(
|
||||
color: Colors.black.withOpacity(0.15),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
// ADDRESS label
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
'ADDRESS',
|
||||
style: TextStyle(
|
||||
color: Colors.black45,
|
||||
fontSize: 7.5.sp,
|
||||
letterSpacing: 1.6,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
SizedBox(height: 6.h),
|
||||
|
||||
// Address line 1
|
||||
Text(
|
||||
'${widget.postcard.cityName},',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: Colors.black87,
|
||||
fontSize: 13.sp,
|
||||
height: 1.5,
|
||||
),
|
||||
),
|
||||
|
||||
SizedBox(height: 6.h),
|
||||
|
||||
// State
|
||||
Text(
|
||||
'${widget.postcard.stateName},',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: Colors.black87,
|
||||
fontSize: 13.sp,
|
||||
height: 1.5,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 6.h),
|
||||
// Country
|
||||
Text(
|
||||
widget.postcard.countryName,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: Colors.black87,
|
||||
fontSize: 13.sp,
|
||||
height: 1.5,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const Spacer(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -58,8 +58,8 @@ class _MyPostCardsViewState extends State<MyPostCardsView> {
|
||||
|
||||
// Handle loaded state
|
||||
if (state is MyPostCardLoaded) {
|
||||
final isDraftsEmpty = state.draftPostCards.isEmpty;
|
||||
final isOrdersEmpty = state.orderPostCards.isEmpty;
|
||||
final isDraftsEmpty = state.allDraftPostCards.isEmpty;
|
||||
final isOrdersEmpty = state.allOrderPostCards.isEmpty;
|
||||
|
||||
developer.log('📊 Loaded - Drafts: ${state.draftPostCards.length}, Orders: ${state.orderPostCards.length}', name: 'MyPostCardsView');
|
||||
developer.log('🔄 Loading - Drafts: ${state.isDraftLoading}, Orders: ${state.isOrderLoading}', name: 'MyPostCardsView');
|
||||
|
||||
@@ -433,6 +433,9 @@ class _PostcardCheckoutPageViewState extends State<PostcardCheckoutPageView> {
|
||||
color: checkoutState.isLoading
|
||||
? Colors.grey
|
||||
: const Color(0xffF95F62),
|
||||
decoration:TextDecoration.underline,
|
||||
decorationColor: const Color(0xffF95F62),
|
||||
decorationThickness: 2 ,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'dart:io';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
@@ -208,12 +209,15 @@ class _PostcardPurchaseFormPageViewState extends State<PostcardPurchaseFormPageV
|
||||
hint: "eg: Jay@gmail.com",
|
||||
controller: _emailController,
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
isEmail: true,
|
||||
),
|
||||
_buildInputField(
|
||||
label: "Phone number",
|
||||
hint: "eg: +91 9999 999 999",
|
||||
hint: "eg: 9999 999 999",
|
||||
controller: _phoneController,
|
||||
keyboardType: TextInputType.phone,
|
||||
keyboardType: TextInputType.number,
|
||||
maxLength: 10,
|
||||
isMobileNumber: true,
|
||||
),
|
||||
_buildInputField(
|
||||
label: "Address",
|
||||
@@ -240,6 +244,7 @@ class _PostcardPurchaseFormPageViewState extends State<PostcardPurchaseFormPageV
|
||||
hint: "Enter the Zip Code you reside in",
|
||||
controller: _zipCodeController,
|
||||
keyboardType: TextInputType.number,
|
||||
maxLength: 6,
|
||||
),
|
||||
_buildDropdownField(
|
||||
label: "Country",
|
||||
@@ -348,6 +353,10 @@ class _PostcardPurchaseFormPageViewState extends State<PostcardPurchaseFormPageV
|
||||
required TextEditingController controller,
|
||||
IconData? icon,
|
||||
TextInputType? keyboardType,
|
||||
int? maxLength,
|
||||
bool isEmail = false,
|
||||
bool isMobileNumber = false, // ✅ NEW
|
||||
int mobileLength = 10, // ✅ NEW (default 10)
|
||||
}) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 18),
|
||||
@@ -365,9 +374,17 @@ class _PostcardPurchaseFormPageViewState extends State<PostcardPurchaseFormPageV
|
||||
const SizedBox(height: 6),
|
||||
TextFormField(
|
||||
controller: controller,
|
||||
keyboardType: keyboardType,
|
||||
keyboardType: keyboardType ??
|
||||
(isMobileNumber
|
||||
? TextInputType.phone
|
||||
: TextInputType.text),
|
||||
maxLength: maxLength ?? (isMobileNumber ? mobileLength : null),
|
||||
inputFormatters: isMobileNumber
|
||||
? [FilteringTextInputFormatter.digitsOnly]
|
||||
: null,
|
||||
decoration: InputDecoration(
|
||||
hintText: hint,
|
||||
counterText: "",
|
||||
hintStyle: GoogleFonts.poppins(
|
||||
color: const Color(0xff999999),
|
||||
fontSize: 14.sp,
|
||||
@@ -395,12 +412,28 @@ class _PostcardPurchaseFormPageViewState extends State<PostcardPurchaseFormPageV
|
||||
),
|
||||
),
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
if (value == null || value.trim().isEmpty) {
|
||||
return 'Please enter $label';
|
||||
}
|
||||
if (label == "Email ID" && !value.contains('@')) {
|
||||
return 'Please enter a valid email';
|
||||
|
||||
if (isEmail) {
|
||||
final emailRegex = RegExp(
|
||||
r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$',
|
||||
);
|
||||
if (!emailRegex.hasMatch(value.trim())) {
|
||||
return 'Please enter a valid email address';
|
||||
}
|
||||
}
|
||||
|
||||
if (isMobileNumber) {
|
||||
if (!RegExp(r'^\d+$').hasMatch(value)) {
|
||||
return 'Only numbers are allowed';
|
||||
}
|
||||
if (value.length != mobileLength) {
|
||||
return 'Mobile number must be $mobileLength digits';
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
),
|
||||
|
||||
257
lib/postcard/widgets/edit_post_card/edit_message.dart
Normal file
257
lib/postcard/widgets/edit_post_card/edit_message.dart
Normal file
@@ -0,0 +1,257 @@
|
||||
import 'package:citycards_customer/postcard/views/write_message_step_page_view.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:html/parser.dart' as html_parser;
|
||||
|
||||
class EditMessage extends StatefulWidget {
|
||||
final String text;
|
||||
final Function(String, String) onChange;
|
||||
const EditMessage({super.key, required this.text, required this.onChange});
|
||||
|
||||
@override
|
||||
State<EditMessage> createState() => _EditMessageState();
|
||||
}
|
||||
|
||||
class _EditMessageState extends State<EditMessage> {
|
||||
final TextEditingController _controller = TextEditingController();
|
||||
final fonts = [
|
||||
{"name": "Default", "font": GoogleFonts.poppins(), "cleanName": "Poppins"},
|
||||
{
|
||||
"name": "Patrick Hand",
|
||||
"font": GoogleFonts.patrickHand(),
|
||||
"cleanName": "Patrick Hand",
|
||||
},
|
||||
{
|
||||
"name": "Indie Flower",
|
||||
"font": GoogleFonts.indieFlower(),
|
||||
"cleanName": "Indie Flower",
|
||||
},
|
||||
{
|
||||
"name": "Gloria Hallelujah",
|
||||
"font": GoogleFonts.gloriaHallelujah(),
|
||||
"cleanName": "Gloria Hallelujah",
|
||||
},
|
||||
];
|
||||
|
||||
String selectedFont = "Poppins";
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
final parsedMessage = _parseHtmlMessage(widget.text);
|
||||
final messageText = parsedMessage['text'] ?? '';
|
||||
final fontFamily = parsedMessage['fontFamily'] ?? '';
|
||||
setState(() {
|
||||
_controller.text = messageText;
|
||||
selectedFont = fontFamily;
|
||||
});
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFFFF5F5),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: CustomPaint(
|
||||
painter: LinedPaperPainter(),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
child: TextField(
|
||||
controller: _controller,
|
||||
maxLines: 8,
|
||||
maxLength: 400,
|
||||
cursorColor: const Color(0xffF95F62),
|
||||
style: _getTextFieldStyle(selectedFont, fonts),
|
||||
decoration: InputDecoration(
|
||||
border: InputBorder.none,
|
||||
hintText: "Add Your Message Here",
|
||||
hintStyle: TextStyle(
|
||||
color: const Color(0xff999999),
|
||||
fontSize: 14.sp,
|
||||
),
|
||||
counterText: "",
|
||||
),
|
||||
onChanged: (val) {
|
||||
widget.onChange(val, selectedFont);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment.bottomRight,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(top: 6, right: 8),
|
||||
child: Text(
|
||||
"${_controller.text.length}/400",
|
||||
style: TextStyle(fontSize: 12.sp, color: const Color(0xff999999)),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
|
||||
SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(
|
||||
children: fonts.map((font) {
|
||||
final TextStyle fontStyle = font['font'] as TextStyle;
|
||||
final String fontName = font["name"] as String;
|
||||
final String cleanName = font["cleanName"] as String;
|
||||
|
||||
final isSelected = selectedFont == cleanName;
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
setState(() {
|
||||
selectedFont = cleanName;
|
||||
});
|
||||
widget.onChange(_controller.text, selectedFont);
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(6),
|
||||
width: 100.w,
|
||||
height: 100.h,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: CustomPaint(
|
||||
painter: DottedBorderPainter(
|
||||
color: isSelected
|
||||
? const Color(0xffF95F62)
|
||||
: const Color(0xffE0E0E0),
|
||||
strokeWidth: 1.5,
|
||||
dashWidth: 4,
|
||||
dashSpace: 3,
|
||||
borderRadius: 12,
|
||||
),
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
"Aa",
|
||||
style: fontStyle.copyWith(
|
||||
fontSize: 24.sp,
|
||||
color: const Color(0xff1A1A1A),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
fontName,
|
||||
textAlign: TextAlign.center,
|
||||
style: fontStyle.copyWith(
|
||||
fontSize: 11.sp,
|
||||
color: isSelected
|
||||
? const Color(0xffF95F62)
|
||||
: const Color(0xff2D3134),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
TextStyle _getTextFieldStyle(
|
||||
String? selectedFont,
|
||||
List<Map<String, dynamic>> fonts,
|
||||
) {
|
||||
if (selectedFont == null || selectedFont.isEmpty) {
|
||||
return GoogleFonts.poppins(fontSize: 14.sp, color: Colors.black);
|
||||
}
|
||||
|
||||
// Find matching font by cleanName
|
||||
for (var font in fonts) {
|
||||
if (font['cleanName'] == selectedFont) {
|
||||
final TextStyle fontStyle = font['font'] as TextStyle;
|
||||
return fontStyle.copyWith(fontSize: 14.sp, color: Colors.black);
|
||||
}
|
||||
}
|
||||
|
||||
// Default fallback to Poppins
|
||||
return GoogleFonts.poppins(fontSize: 14.sp, color: Colors.black);
|
||||
}
|
||||
|
||||
Map<String, String> _parseHtmlMessage(String htmlMessage) {
|
||||
if (htmlMessage.isEmpty) {
|
||||
return {'text': '', 'fontFamily': ''};
|
||||
}
|
||||
|
||||
// Check if message contains HTML tags
|
||||
if (!htmlMessage.contains('<span') && !htmlMessage.contains('style=')) {
|
||||
// Plain text message - no font specified
|
||||
return {'text': htmlMessage, 'fontFamily': ''};
|
||||
}
|
||||
|
||||
try {
|
||||
// Parse HTML
|
||||
final document = html_parser.parse(htmlMessage);
|
||||
final spanElement = document.querySelector('span');
|
||||
|
||||
if (spanElement != null) {
|
||||
// Extract text content
|
||||
final text = spanElement.text;
|
||||
|
||||
// Extract font-family from style attribute
|
||||
final style = spanElement.attributes['style'] ?? '';
|
||||
final fontFamilyMatch = RegExp(
|
||||
r'font-family:\s*([^;]+)',
|
||||
).firstMatch(style);
|
||||
final fontFamily = fontFamilyMatch?.group(1)?.trim() ?? '';
|
||||
|
||||
return {'text': text, 'fontFamily': fontFamily};
|
||||
}
|
||||
|
||||
// Fallback: return plain text
|
||||
return {'text': document.body?.text ?? htmlMessage, 'fontFamily': ''};
|
||||
} catch (e) {
|
||||
// If parsing fails, return original message
|
||||
return {'text': htmlMessage, 'fontFamily': ''};
|
||||
}
|
||||
}
|
||||
|
||||
// Get TextStyle with any Google Font
|
||||
TextStyle _getFontStyle(String fontFamily, double fontSize, double height) {
|
||||
// If no font family specified, use default Caveat
|
||||
if (fontFamily.isEmpty) {
|
||||
return GoogleFonts.caveat(fontSize: fontSize, height: height);
|
||||
}
|
||||
|
||||
try {
|
||||
// Normalize font name: remove extra spaces, handle common variations
|
||||
final normalizedFont = fontFamily.trim().replaceAll(
|
||||
RegExp(r'\s+'),
|
||||
' ',
|
||||
); // Replace multiple spaces with single space
|
||||
|
||||
// Try to get the font from Google Fonts
|
||||
// GoogleFonts.getFont() can load ANY Google Font dynamically
|
||||
return GoogleFonts.getFont(
|
||||
normalizedFont,
|
||||
fontSize: fontSize,
|
||||
height: height,
|
||||
);
|
||||
} catch (e) {
|
||||
// If font not found in Google Fonts, fallback to default
|
||||
debugPrint(
|
||||
'⚠️ Font "$fontFamily" not found in Google Fonts. Using default Caveat font.',
|
||||
);
|
||||
return GoogleFonts.caveat(fontSize: fontSize, height: height);
|
||||
}
|
||||
}
|
||||
}
|
||||
276
lib/postcard/widgets/edit_post_card/your_details.dart
Normal file
276
lib/postcard/widgets/edit_post_card/your_details.dart
Normal file
@@ -0,0 +1,276 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
|
||||
class EditYourdetails extends StatefulWidget {
|
||||
final TextEditingController fullNameController;
|
||||
final TextEditingController addressController;
|
||||
final TextEditingController cityController;
|
||||
final TextEditingController zipCodeController;
|
||||
final String selectedCountry;
|
||||
final String selectedState;
|
||||
final GlobalKey<FormState> formKey;
|
||||
final Function(String) selectState;
|
||||
final Function(String) selectCountry;
|
||||
const EditYourdetails({
|
||||
super.key,
|
||||
required this.fullNameController,
|
||||
required this.addressController,
|
||||
required this.cityController,
|
||||
required this.zipCodeController,
|
||||
required this.selectedCountry,
|
||||
required this.selectedState,
|
||||
required this.formKey,
|
||||
required this.selectState,
|
||||
required this.selectCountry,
|
||||
});
|
||||
|
||||
@override
|
||||
State<EditYourdetails> createState() => _EditYourdetailsState();
|
||||
}
|
||||
|
||||
class _EditYourdetailsState extends State<EditYourdetails> {
|
||||
String? _selectedState;
|
||||
String? _selectedCountry;
|
||||
|
||||
final List<String> countries = ['Australia'];
|
||||
|
||||
final List<String> states = [
|
||||
'New South Wales',
|
||||
'Victoria',
|
||||
'Queensland',
|
||||
'South Australia',
|
||||
'Western Australia',
|
||||
'Tasmania',
|
||||
'Northern Territory',
|
||||
'Australian Capital Territory',
|
||||
];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
setState(() {
|
||||
_selectedState = states.contains(widget.selectedState)
|
||||
? widget.selectedState
|
||||
: null;
|
||||
_selectedCountry = countries.contains(widget.selectedCountry)
|
||||
? widget.selectedCountry
|
||||
: null;
|
||||
});
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"Recipient Details",
|
||||
style: GoogleFonts.poppins(
|
||||
color: Color(0XFF212121),
|
||||
fontSize: 18.sp,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 2.h),
|
||||
Text(
|
||||
"Enter the address of the person who will receive this postcard",
|
||||
style: GoogleFonts.poppins(
|
||||
color: Color(0XFF000000).withValues(alpha: 0.6),
|
||||
fontSize: 14.sp,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
_buildInputField(
|
||||
label: "Recipient",
|
||||
hint: "Enter the recipient's name",
|
||||
controller: widget.fullNameController,
|
||||
),
|
||||
_buildInputField(
|
||||
label: "Address",
|
||||
hint: "Enter the recipient's Address",
|
||||
controller: widget.addressController,
|
||||
),
|
||||
_buildInputField(
|
||||
label: "City",
|
||||
hint: "Enter the name of your city",
|
||||
controller: widget.cityController,
|
||||
),
|
||||
_buildDropdownField(
|
||||
label: "Country",
|
||||
hint: "Select your country",
|
||||
value: _selectedCountry,
|
||||
items: countries,
|
||||
onChanged: (val) {
|
||||
setState(() {
|
||||
_selectedCountry = val;
|
||||
});
|
||||
widget.selectCountry(val!);
|
||||
},
|
||||
),
|
||||
_buildDropdownField(
|
||||
label: "State",
|
||||
hint: "Select your state",
|
||||
value: _selectedState,
|
||||
items: states,
|
||||
onChanged: (val) {
|
||||
setState(() {
|
||||
_selectedState = val;
|
||||
});
|
||||
widget.selectState(val!);
|
||||
},
|
||||
),
|
||||
_buildInputField(
|
||||
label: "Zip Code",
|
||||
hint: "Enter the Zip Code you reside in",
|
||||
controller: widget.zipCodeController,
|
||||
keyboardType: TextInputType.number,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildInputField({
|
||||
required String label,
|
||||
required String hint,
|
||||
required TextEditingController controller,
|
||||
IconData? icon,
|
||||
TextInputType? keyboardType,
|
||||
}) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 18),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 13.sp,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: const Color(0xff1A1A1A),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
TextFormField(
|
||||
controller: controller,
|
||||
keyboardType: keyboardType,
|
||||
decoration: InputDecoration(
|
||||
hintText: hint,
|
||||
hintStyle: GoogleFonts.poppins(
|
||||
color: const Color(0xff999999),
|
||||
fontSize: 14.sp,
|
||||
),
|
||||
suffixIcon: icon != null
|
||||
? Icon(icon, color: Colors.black, size: 20)
|
||||
: null,
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
vertical: 14,
|
||||
horizontal: 12,
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderSide: const BorderSide(color: Color(0xffFDCDCE)),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderSide: const BorderSide(color: Color(0xffFDCDCE)),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
errorBorder: OutlineInputBorder(
|
||||
borderSide: const BorderSide(color: Colors.red),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
focusedErrorBorder: OutlineInputBorder(
|
||||
borderSide: const BorderSide(color: Colors.red),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Please enter $label';
|
||||
}
|
||||
if (label == "Email ID" && !value.contains('@')) {
|
||||
return 'Please enter a valid email';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDropdownField({
|
||||
required String label,
|
||||
required String hint,
|
||||
required String? value,
|
||||
required List<String> items,
|
||||
required Function(String?) onChanged,
|
||||
}) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 18),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 13.sp,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: const Color(0xff1A1A1A),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
DropdownButtonFormField<String>(
|
||||
value: value,
|
||||
decoration: InputDecoration(
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
vertical: 14,
|
||||
horizontal: 12,
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderSide: const BorderSide(color: Color(0xffFDCDCE)),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderSide: const BorderSide(color: Color(0xffFDCDCE)),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
errorBorder: OutlineInputBorder(
|
||||
borderSide: const BorderSide(color: Colors.red),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
focusedErrorBorder: OutlineInputBorder(
|
||||
borderSide: const BorderSide(color: Colors.red),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
icon: const Icon(
|
||||
Icons.keyboard_arrow_down,
|
||||
color: Color(0xffFDCDCE),
|
||||
),
|
||||
hint: Text(
|
||||
hint,
|
||||
style: GoogleFonts.poppins(
|
||||
color: const Color(0xff999999),
|
||||
fontSize: 14.sp,
|
||||
),
|
||||
),
|
||||
items: items.map((String item) {
|
||||
return DropdownMenuItem<String>(value: item, child: Text(item));
|
||||
}).toList(),
|
||||
onChanged: onChanged,
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Please select $label';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -496,6 +496,8 @@ class _EditProfilePageState extends State<EditProfilePage> {
|
||||
hint: "Enter your phone number",
|
||||
controller: phoneController,
|
||||
enabled: !isLoading,
|
||||
keyboardType: TextInputType.number,
|
||||
maxLength: 10,
|
||||
validator: (value) {
|
||||
if (value == null || value.trim().isEmpty) {
|
||||
return 'Phone number is required';
|
||||
@@ -679,6 +681,8 @@ class _EditProfilePageState extends State<EditProfilePage> {
|
||||
hint: "Enter the ZIP code you reside in",
|
||||
controller: zipCodeController,
|
||||
enabled: !isLoading,
|
||||
keyboardType: TextInputType.number,
|
||||
maxLength: 6,
|
||||
),
|
||||
),
|
||||
|
||||
|
||||
10
pubspec.lock
10
pubspec.lock
@@ -371,6 +371,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.9.3"
|
||||
flutter_slidable:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_slidable
|
||||
sha256: ea369262929d3cc6ebf9d8a00c196127966f117fe433a5e5cb47fb08008ca203
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.0.3"
|
||||
flutter_stripe:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -822,7 +830,7 @@ packages:
|
||||
source: hosted
|
||||
version: "1.9.1"
|
||||
path_provider:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: path_provider
|
||||
sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
|
||||
|
||||
@@ -61,6 +61,8 @@ dependencies:
|
||||
cached_network_image: ^3.4.1
|
||||
bloc: ^9.2.0
|
||||
csc_picker_plus: ^0.0.3
|
||||
flutter_slidable: ^4.0.3
|
||||
path_provider: ^2.1.5
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
||||
Reference in New Issue
Block a user