Compare commits
2 Commits
48fd7037ea
...
0abdd2b796
| Author | SHA1 | Date | |
|---|---|---|---|
| 0abdd2b796 | |||
| dd1991da09 |
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,
|
||||
),
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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,12 +53,10 @@ class _LoginEmailBottomsheetState extends State<LoginEmailBottomsheet> {
|
||||
),
|
||||
);
|
||||
} else if (state is LoginError) {
|
||||
Navigator.pop(context);
|
||||
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
|
||||
);
|
||||
}
|
||||
},
|
||||
@@ -117,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,12 +78,10 @@ class _VerifyOtpBottomsheetState extends State<VerifyOtpBottomsheet> {
|
||||
),
|
||||
);
|
||||
} else if (state is VerifyOtpError) {
|
||||
Navigator.pop(context);
|
||||
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
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -12,20 +12,24 @@ class MyPostCardBloc extends Bloc<MyPostCardEvent, MyPostCardState> {
|
||||
final MyPostCardsRepository repository;
|
||||
|
||||
MyPostCardBloc({required this.repository})
|
||||
: super(const MyPostCardInitial()) {
|
||||
: 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
|
||||
Future<void> _onCheckLoginStatus(
|
||||
CheckLoginStatus event,
|
||||
Emitter<MyPostCardState> emit,
|
||||
) async {
|
||||
CheckLoginStatus event,
|
||||
Emitter<MyPostCardState> emit,
|
||||
) async {
|
||||
developer.log('🔍 Checking login status...', name: 'MyPostCardBloc');
|
||||
emit(const MyPostCardCheckingLogin());
|
||||
|
||||
@@ -68,9 +72,9 @@ class MyPostCardBloc extends Bloc<MyPostCardEvent, MyPostCardState> {
|
||||
|
||||
/// Handle fetching draft postcards
|
||||
Future<void> _onFetchDraftPostCards(
|
||||
FetchDraftPostCards event,
|
||||
Emitter<MyPostCardState> emit,
|
||||
) async {
|
||||
FetchDraftPostCards event,
|
||||
Emitter<MyPostCardState> emit,
|
||||
) async {
|
||||
developer.log('📥 Fetching draft postcards...', name: 'MyPostCardBloc');
|
||||
// Get current state
|
||||
final currentState = state;
|
||||
@@ -88,10 +92,11 @@ class MyPostCardBloc extends Bloc<MyPostCardEvent, MyPostCardState> {
|
||||
);
|
||||
|
||||
if (state is MyPostCardLoaded) {
|
||||
// Update with fetched drafts
|
||||
// Update with fetched drafts and store in allDraftPostCards
|
||||
emit(
|
||||
(state as MyPostCardLoaded).copyWith(
|
||||
draftPostCards: draftPostCards,
|
||||
allDraftPostCards: draftPostCards, // Store original list
|
||||
isDraftLoading: false,
|
||||
),
|
||||
);
|
||||
@@ -105,6 +110,7 @@ class MyPostCardBloc extends Bloc<MyPostCardEvent, MyPostCardState> {
|
||||
MyPostCardLoaded(
|
||||
draftPostCards: draftPostCards,
|
||||
orderPostCards: const [],
|
||||
allDraftPostCards: draftPostCards, // Store original list
|
||||
isDraftLoading: false,
|
||||
isOrderLoading: false,
|
||||
),
|
||||
@@ -123,19 +129,28 @@ class MyPostCardBloc extends Bloc<MyPostCardEvent, MyPostCardState> {
|
||||
}
|
||||
|
||||
Future<void> _onDeletePostCard(
|
||||
DeleteDraftPostCards event,
|
||||
Emitter<MyPostCardState> emit,
|
||||
) async {
|
||||
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);
|
||||
|
||||
List<MyPostCard> items = currentState.draftPostCards;
|
||||
items.removeWhere((e) => e.id == 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: items, isDeleteLoading: false),
|
||||
currentState.copyWith(
|
||||
draftPostCards: filteredItems,
|
||||
allDraftPostCards: allItems,
|
||||
isDeleteLoading: false,
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
log("Erro - $e");
|
||||
@@ -146,9 +161,9 @@ class MyPostCardBloc extends Bloc<MyPostCardEvent, MyPostCardState> {
|
||||
|
||||
/// Handle fetching order postcards
|
||||
Future<void> _onFetchOrderPostCards(
|
||||
FetchOrderPostCards event,
|
||||
Emitter<MyPostCardState> emit,
|
||||
) async {
|
||||
FetchOrderPostCards event,
|
||||
Emitter<MyPostCardState> emit,
|
||||
) async {
|
||||
developer.log('📥 Fetching order postcards...', name: 'MyPostCardBloc');
|
||||
// Get current state
|
||||
final currentState = state;
|
||||
@@ -166,10 +181,11 @@ class MyPostCardBloc extends Bloc<MyPostCardEvent, MyPostCardState> {
|
||||
);
|
||||
|
||||
if (state is MyPostCardLoaded) {
|
||||
// Update with fetched orders
|
||||
// Update with fetched orders and store in allOrderPostCards
|
||||
emit(
|
||||
(state as MyPostCardLoaded).copyWith(
|
||||
orderPostCards: orderPostCards,
|
||||
allOrderPostCards: orderPostCards, // Store original list
|
||||
isOrderLoading: false,
|
||||
),
|
||||
);
|
||||
@@ -183,6 +199,7 @@ class MyPostCardBloc extends Bloc<MyPostCardEvent, MyPostCardState> {
|
||||
MyPostCardLoaded(
|
||||
draftPostCards: const [],
|
||||
orderPostCards: orderPostCards,
|
||||
allOrderPostCards: orderPostCards, // Store original list
|
||||
isDraftLoading: false,
|
||||
isOrderLoading: false,
|
||||
),
|
||||
@@ -202,9 +219,9 @@ class MyPostCardBloc extends Bloc<MyPostCardEvent, MyPostCardState> {
|
||||
|
||||
/// Handle refreshing draft postcards
|
||||
Future<void> _onRefreshDraftPostCards(
|
||||
RefreshDraftPostCards event,
|
||||
Emitter<MyPostCardState> emit,
|
||||
) async {
|
||||
RefreshDraftPostCards event,
|
||||
Emitter<MyPostCardState> emit,
|
||||
) async {
|
||||
developer.log('🔄 Refreshing draft postcards...', name: 'MyPostCardBloc');
|
||||
try {
|
||||
final draftPostCards = await repository.fetchMyPostCards(type: 'draft');
|
||||
@@ -214,9 +231,27 @@ class MyPostCardBloc extends Bloc<MyPostCardEvent, MyPostCardState> {
|
||||
);
|
||||
|
||||
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(
|
||||
@@ -229,9 +264,9 @@ class MyPostCardBloc extends Bloc<MyPostCardEvent, MyPostCardState> {
|
||||
|
||||
/// Handle refreshing order postcards
|
||||
Future<void> _onRefreshOrderPostCards(
|
||||
RefreshOrderPostCards event,
|
||||
Emitter<MyPostCardState> emit,
|
||||
) async {
|
||||
RefreshOrderPostCards event,
|
||||
Emitter<MyPostCardState> emit,
|
||||
) async {
|
||||
developer.log('🔄 Refreshing order postcards...', name: 'MyPostCardBloc');
|
||||
try {
|
||||
final orderPostCards = await repository.fetchMyPostCards(type: 'orders');
|
||||
@@ -241,9 +276,27 @@ class MyPostCardBloc extends Bloc<MyPostCardEvent, MyPostCardState> {
|
||||
);
|
||||
|
||||
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(
|
||||
@@ -253,4 +306,153 @@ class MyPostCardBloc extends Bloc<MyPostCardEvent, MyPostCardState> {
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -36,3 +36,31 @@ class RefreshDraftPostCards extends MyPostCardEvent {
|
||||
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();
|
||||
}
|
||||
@@ -31,13 +31,24 @@ class MyPostCardLoaded extends MyPostCardState {
|
||||
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 => [
|
||||
@@ -46,6 +57,10 @@ class MyPostCardLoaded extends MyPostCardState {
|
||||
isDraftLoading,
|
||||
isOrderLoading,
|
||||
isDeleteLoading,
|
||||
allDraftPostCards,
|
||||
allOrderPostCards,
|
||||
draftSearchQuery,
|
||||
orderSearchQuery,
|
||||
];
|
||||
|
||||
/// Helper method to create a copy with updated values
|
||||
@@ -55,6 +70,10 @@ class MyPostCardLoaded extends MyPostCardState {
|
||||
bool? isDraftLoading,
|
||||
bool? isOrderLoading,
|
||||
bool? isDeleteLoading,
|
||||
List<MyPostCard>? allDraftPostCards,
|
||||
List<MyPostCard>? allOrderPostCards,
|
||||
String? draftSearchQuery,
|
||||
String? orderSearchQuery,
|
||||
}) {
|
||||
return MyPostCardLoaded(
|
||||
draftPostCards: draftPostCards ?? this.draftPostCards,
|
||||
@@ -62,6 +81,10 @@ class MyPostCardLoaded extends MyPostCardState {
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -75,4 +98,4 @@ class MyPostCardError extends MyPostCardState {
|
||||
|
||||
@override
|
||||
List<Object?> get props => [errorMessage, errorType];
|
||||
}
|
||||
}
|
||||
@@ -15,9 +15,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>(
|
||||
@@ -32,7 +45,7 @@ class MyPostCardDraftView extends StatelessWidget {
|
||||
}
|
||||
|
||||
// Show empty state if no drafts
|
||||
if (state.draftPostCards.isEmpty) {
|
||||
if (state.allDraftPostCards.isEmpty) {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 24.w),
|
||||
@@ -82,32 +95,154 @@ class MyPostCardDraftView extends StatelessWidget {
|
||||
}
|
||||
|
||||
// Show the list of drafts
|
||||
return Stack(
|
||||
return Column(
|
||||
children: [
|
||||
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);
|
||||
},
|
||||
// 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
|
||||
},
|
||||
),
|
||||
|
||||
Positioned(
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
child: state.isDeleteLoading == true
|
||||
? Center(
|
||||
// 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,
|
||||
@@ -116,7 +251,10 @@ class MyPostCardDraftView extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
)
|
||||
: SizedBox(),
|
||||
: SizedBox(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
@@ -401,4 +539,4 @@ class MyPostCardDraftView extends StatelessWidget {
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>(
|
||||
@@ -28,7 +41,7 @@ class MyPostCardOrdersView extends StatelessWidget {
|
||||
}
|
||||
|
||||
// Show empty state if no orders
|
||||
if (state.orderPostCards.isEmpty) {
|
||||
if (state.allOrderPostCards.isEmpty) {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 24.w),
|
||||
@@ -77,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),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -384,4 +539,4 @@ class MyPostCardOrdersView extends StatelessWidget {
|
||||
return status;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
},
|
||||
),
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
|
||||
|
||||
Reference in New Issue
Block a user