2 Commits

Author SHA1 Message Date
0abdd2b796 validations added 2026-02-16 13:43:24 +05:30
dd1991da09 search added in my drafts and y orders 2026-02-16 12:58:17 +05:30
15 changed files with 1384 additions and 556 deletions

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -19,6 +19,10 @@ class MyPostCardBloc extends Bloc<MyPostCardEvent, MyPostCardState> {
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
@@ -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,
),
@@ -132,10 +138,19 @@ class MyPostCardBloc extends Bloc<MyPostCardEvent, MyPostCardState> {
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");
@@ -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,
),
@@ -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(
@@ -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();
}
}

View File

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

View File

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

View File

@@ -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,7 +95,86 @@ class MyPostCardDraftView extends StatelessWidget {
}
// Show the list of drafts
return Stack(
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 {
@@ -91,8 +183,51 @@ class MyPostCardDraftView extends StatelessWidget {
);
},
color: const Color(0xffF95F62),
child: ListView.builder(
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];
@@ -119,6 +254,9 @@ class MyPostCardDraftView extends StatelessWidget {
: SizedBox(),
),
],
),
),
],
);
}

View File

@@ -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(
// 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());
context.read<MyPostCardBloc>().add(
const RefreshOrderPostCards(),
);
},
color: const Color(0xffF95F62),
child: ListView.builder(
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),
),
),
),
],
),
),
],
);
}

View File

@@ -1,14 +1,19 @@
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 {
class MyPostcardPreviewView extends StatefulWidget {
final MyPostCard postcard;
const MyPostcardPreviewView({
@@ -18,9 +23,9 @@ class MyPostcardPreviewView extends StatefulWidget {
@override
State<MyPostcardPreviewView> createState() => _MyPostcardPreviewViewState();
}
}
class _MyPostcardPreviewViewState extends State<MyPostcardPreviewView> {
class _MyPostcardPreviewViewState extends State<MyPostcardPreviewView> {
bool showBack = false;
@override
@@ -77,8 +82,22 @@ class _MyPostcardPreviewViewState extends State<MyPostcardPreviewView> {
),
SizedBox(width: 16.w),
GestureDetector(
onTap: () {
// Edit functionality
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',
@@ -462,4 +481,4 @@ class _MyPostcardPreviewViewState extends State<MyPostcardPreviewView> {
),
);
}
}
}

View File

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

View File

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

View File

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

View File

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