3 Commits

Author SHA1 Message Date
Shreeyash Thorat
cdfb9c74ca upload, edit postcard image with filter 2026-02-17 15:15:21 +05:30
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
30 changed files with 2406 additions and 669 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

@@ -1,8 +1,9 @@
class ApiUrls {
// static const baseUrl = "https://devapi.citycards.betadelivery.com";//Normal API
// static const baseUrl = "https://testingapi.citycards.betadelivery.com";// Test API
static const baseUrl = "https://uatapi.citycard.betadelivery.com";// Production Lvl API
static const baseUrl =
"https://devapi.citycards.betadelivery.com"; //Normal API
//static const baseUrl =
// "https://testingapi.citycards.betadelivery.com"; // Test API
// static const baseUrl = "https://uatapi.citycard.betadelivery.com";// Production Lvl API
static const refreshToken = "$baseUrl/auth/refresh";

View File

@@ -0,0 +1,150 @@
import 'dart:developer';
import 'dart:io';
import 'package:bloc/bloc.dart';
import 'package:dio/dio.dart';
import 'package:equatable/equatable.dart';
import 'package:path_provider/path_provider.dart';
import 'package:image/image.dart' as img;
part 'edit_image_filter_event.dart';
part 'edit_image_filter_state.dart';
enum EditImageType { network, file }
class EditImageFilterBloc
extends Bloc<EditImageFilterEvent, EditImageFilterState> {
EditImageFilterBloc() : super(EditImageFilterInitial()) {
on<DownloadImage>((event, emit) async {
try {
emit(DownloadImageLoading());
if (event.type == EditImageType.network) {
final Dio dio = Dio();
final directory = await getTemporaryDirectory();
final String fileName =
'${DateTime.now().millisecondsSinceEpoch}.png';
final String filePath = '${directory.path}/$fileName';
await dio.download(
event.url,
filePath,
options: Options(responseType: ResponseType.bytes),
);
emit(
DownloadImageSuccessfully(
filePath: filePath,
filteredImagePath: filePath,
filter: 'original',
),
);
} else {
emit(
DownloadImageSuccessfully(
filePath: event.url,
filteredImagePath: event.url,
filter: 'original',
),
);
}
} catch (e) {
emit(DownloadImageFailed());
}
});
on<SelectFilter>((event, emit) async {
if (state is! DownloadImageSuccessfully) return;
final currentState = state as DownloadImageSuccessfully;
try {
log("Selected Filter ${event.filterName}");
emit(currentState.copyWith(processing: true));
if (event.filterName == "none" || event.filterName == "original") {
emit(
currentState.copyWith(
filteredImagePath: currentState.filePath,
processing: false,
filter: "original",
),
);
return;
}
final originalFile = File(currentState.filePath);
final bytes = await originalFile.readAsBytes();
img.Image? image = img.decodeImage(bytes);
if (image == null) {
emit(currentState.copyWith(processing: false));
return;
}
switch (event.filterName) {
case "vintage":
image = img.adjustColor(
image,
saturation: 0.8,
gamma: 1.1,
contrast: 0.9,
);
break;
case "bw":
image = img.grayscale(image);
break;
case "sepia":
image = img.sepia(image);
break;
case "cool":
// hue is normalized 0.01.0; -15 degrees ≈ -15/360 ≈ -0.042
image = img.adjustColor(image, hue: -0.042, contrast: 1.05);
break;
case "contrast":
image = img.adjustColor(image, contrast: 1.4);
break;
case "soft":
image = img.adjustColor(
image,
brightness: 0.1,
gamma: 0.9,
saturation: 1.1,
);
break;
default:
emit(currentState.copyWith(filter: "none", processing: false));
return;
}
final filteredPath =
"${originalFile.parent.path}/filtered_${event.filterName}_${DateTime.now().millisecondsSinceEpoch}.jpg";
final filteredFile = File(filteredPath)
..writeAsBytesSync(img.encodeJpg(image, quality: 95));
if (currentState.filteredImagePath != currentState.filePath) {
final oldFile = File(currentState.filteredImagePath);
if (await oldFile.exists()) await oldFile.delete();
}
log(
"Filter applied: ${filteredFile.path} | filter: ${event.filterName}",
);
emit(
currentState.copyWith(
filteredImagePath: filteredFile.path,
filter: event.filterName,
processing: false,
),
);
return;
} catch (e) {
log("SelectFilter error: ${e.toString()}");
emit(currentState.copyWith(processing: false)); // don't leave UI stuck
}
});
}
}

View File

@@ -0,0 +1,19 @@
part of 'edit_image_filter_bloc.dart';
class EditImageFilterEvent extends Equatable {
const EditImageFilterEvent();
@override
List<Object> get props => [];
}
class DownloadImage extends EditImageFilterEvent {
final String url;
final EditImageType type;
const DownloadImage({required this.url, required this.type});
}
class SelectFilter extends EditImageFilterEvent {
final String filterName;
const SelectFilter({required this.filterName});
}

View File

@@ -0,0 +1,44 @@
part of 'edit_image_filter_bloc.dart';
class EditImageFilterState extends Equatable {
const EditImageFilterState();
@override
List<Object> get props => [];
}
class EditImageFilterInitial extends EditImageFilterState {}
class DownloadImageLoading extends EditImageFilterState {}
class DownloadImageSuccessfully extends EditImageFilterState {
final String filePath;
final String filteredImagePath;
final bool processing;
final String filter;
const DownloadImageSuccessfully({
required this.filePath,
required this.filteredImagePath,
this.processing = false,
required this.filter,
});
@override
List<Object> get props => [filePath, filteredImagePath, processing, filter];
DownloadImageSuccessfully copyWith({
String? filePath,
String? filteredImagePath,
bool? processing,
String? filter,
}) {
return DownloadImageSuccessfully(
filePath: filePath ?? this.filePath,
filteredImagePath: filteredImagePath ?? this.filteredImagePath,
processing: processing ?? this.processing,
filter: filter ?? this.filter,
);
}
}
class DownloadImageFailed extends EditImageFilterState {}

View File

@@ -15,6 +15,7 @@ class EditPostcardBloc extends Bloc<EditPostcardEvent, EditPostcardState> {
emit(EditPostcardLoading());
await MyPostCardsRepository().editMyPostCards(
postcard: event.myPostCard,
image: event.editImage,
);
log("Edit PostCard Successfully");
emit(EditPostcardSuccessfull());

View File

@@ -9,5 +9,6 @@ class EditPostcardEvent extends Equatable {
class EditPostCard extends EditPostcardEvent {
final MyPostCard myPostCard;
const EditPostCard({required this.myPostCard});
final String? editImage;
const EditPostCard({required this.myPostCard, this.editImage});
}

View File

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

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,
);
}
}
@@ -75,4 +98,4 @@ class MyPostCardError extends MyPostCardState {
@override
List<Object?> get props => [errorMessage, errorType];
}
}

View File

@@ -0,0 +1,80 @@
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:image_picker/image_picker.dart';
part 'pick_images_event.dart';
part 'pick_images_state.dart';
class PickImagesBloc extends Bloc<PickImagesEvent, PickImagesState> {
PickImagesBloc() : super(PickImagesState()) {
final ImagePicker imagePicker = ImagePicker();
on<TakePhoto>((event, emit) async {
PickImagesState currentState = state;
try {
emit(currentState.copyWith(loading: true));
final XFile? pickedFile = await imagePicker.pickImage(
source: ImageSource.camera,
imageQuality: 80,
maxWidth: 1920,
maxHeight: 1920,
);
if (pickedFile != null) {
emit(
currentState.copyWith(
loading: false,
file: pickedFile.path,
filteredFile: pickedFile.path,
),
);
} else {
emit(currentState.copyWith(loading: false));
}
} catch (e) {
emit(currentState.copyWith(loading: false));
}
});
on<PickPhoto>((event, emit) async {
PickImagesState currentState = state;
try {
emit(currentState.copyWith(loading: true));
final XFile? pickedFile = await imagePicker.pickImage(
source: ImageSource.gallery,
imageQuality: 80,
maxWidth: 1920,
maxHeight: 1920,
);
if (pickedFile != null) {
emit(
currentState.copyWith(
loading: false,
file: pickedFile.path,
filteredFile: pickedFile.path,
),
);
} else {
emit(currentState.copyWith(loading: false));
}
} catch (e) {
emit(currentState.copyWith(loading: false));
}
});
on<SelectedFilter>((event, emit) async {
PickImagesState currentState = state;
try {
emit(
currentState.copyWith(
loading: false,
file: currentState.file ?? event.imagePath,
filteredFile: event.imagePath,
),
);
} catch (e) {
emit(currentState.copyWith(loading: false));
}
});
}
}

View File

@@ -0,0 +1,19 @@
part of 'pick_images_bloc.dart';
class PickImagesEvent extends Equatable {
const PickImagesEvent();
@override
List<Object> get props => [];
}
class TakePhoto extends PickImagesEvent {}
class PickPhoto extends PickImagesEvent {}
class RemovePhoto extends PickImagesEvent {}
class SelectedFilter extends PickImagesEvent {
final String imagePath;
const SelectedFilter({required this.imagePath});
}

View File

@@ -0,0 +1,23 @@
part of 'pick_images_bloc.dart';
class PickImagesState extends Equatable {
final String? file;
final String? filteredFile;
final bool? loading;
const PickImagesState({this.file, this.loading = false, this.filteredFile});
PickImagesState copyWith({
String? file,
bool? loading,
String? filteredFile,
}) {
return PickImagesState(
file: file ?? this.file,
loading: loading ?? this.loading,
filteredFile: filteredFile ?? this.filteredFile,
);
}
@override
List<dynamic> get props => [file, filteredFile, loading];
}

View File

@@ -20,7 +20,10 @@ class MyPostCardsRepository {
return (response.data as List).map((e) => MyPostCard.fromJson(e)).toList();
}
Future<void> editMyPostCards({required MyPostCard postcard}) async {
Future<void> editMyPostCards({
required MyPostCard postcard,
String? image,
}) async {
try {
final formData = FormData();
@@ -44,16 +47,15 @@ class MyPostCardsRepository {
if (postcard.address2.isNotEmpty) {
formData.fields.add(MapEntry('address2', postcard.address2));
}
// final fileName = postcard.pcImagePath.split('/').last;
// formData.files.add(
// MapEntry(
// 'pcImage',
// await MultipartFile.fromFile(
// postcard.pcImagePath,
// filename: fileName,
// ),
// ),
// );
if (image != null && image.isNotEmpty) {
final fileName = image.split('/').last;
formData.files.add(
MapEntry(
'pcImage',
await MultipartFile.fromFile(image, filename: fileName),
),
);
}
await _apiService.putApi(
url: '${ApiUrls.editPostcard}/${postcard.id}',
data: formData,

View File

@@ -0,0 +1,424 @@
import 'dart:io';
import 'package:citycards_customer/common_packages/app_bar.dart';
import 'package:citycards_customer/postcard/blocs/edit_image_filter/edit_image_filter_bloc.dart';
import 'package:citycards_customer/postcard/blocs/pick_images/pick_images_bloc.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import '../widgets/dotted_border_holder.dart';
class EditImageFilter extends StatefulWidget {
final EditImageType type;
final String url;
final PickImagesBloc pickImagesBloc;
const EditImageFilter({
super.key,
required this.type,
required this.url,
required this.pickImagesBloc,
});
@override
State<EditImageFilter> createState() => _EditImageFilterState();
}
class _EditImageFilterState extends State<EditImageFilter> {
final EditImageFilterBloc editImageFilterBloc = EditImageFilterBloc();
@override
void initState() {
editImageFilterBloc.add(DownloadImage(url: widget.url, type: widget.type));
super.initState();
}
@override
Widget build(BuildContext context) {
return BlocConsumer<EditImageFilterBloc, EditImageFilterState>(
bloc: editImageFilterBloc,
listener: (ctx, state) {
if (state is DownloadImageFailed) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("Failed to fetch edit details")),
);
Navigator.pop(context);
}
},
builder: (context, state) {
if (state is DownloadImageLoading) {
return const Scaffold(
body: SafeArea(child: Center(child: CircularProgressIndicator())),
);
}
if (state is DownloadImageSuccessfully) {
return Scaffold(
body: SafeArea(
child: Stack(
children: [
SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
CommonAppBar(
isWhiteLogo: false,
isProfilePage: false,
showDivider: true,
),
GestureDetector(
onTap: () => Navigator.pop(context),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 16),
child: Row(
children: [
const Icon(Icons.arrow_back, size: 20),
const SizedBox(width: 8),
const Text(
"Back",
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
],
),
),
),
const Text(
"Add a Filter",
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 6),
Text(
"Choose your favorite filter and enhance your postcard.",
style: TextStyle(
fontSize: 14.sp,
fontWeight: FontWeight.w400,
color: const Color(0xff2D3134),
),
),
const SizedBox(height: 10),
DottedBorderContainerHolder(
imagePath: state.filteredImagePath,
filter: state.filter,
),
const SizedBox(height: 20),
SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: [
buildFilterOption(
context,
"Original",
File(state.filePath),
"original",
state.filter == "original",
),
buildFilterOption(
context,
"Black & White",
File(state.filePath),
"bw",
state.filter == "bw",
),
buildFilterOption(
context,
"Sepia",
File(state.filePath),
"sepia",
state.filter == "sepia",
),
buildFilterOption(
context,
"Vintage",
File(state.filePath),
"vintage",
state.filter == "vintage",
),
buildFilterOption(
context,
"Cool Tone",
File(state.filePath),
"cool",
state.filter == "cool",
),
buildFilterOption(
context,
"Contrast",
File(state.filePath),
"contrast",
state.filter == "contrast",
),
buildFilterOption(
context,
"Soft Glow",
File(state.filePath),
"soft",
state.filter == "soft",
),
],
),
),
SizedBox(height: 20.h),
SizedBox(
width: double.infinity,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xffF95F62),
padding: EdgeInsets.symmetric(vertical: 16.h),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(40),
),
),
onPressed: () {
widget.pickImagesBloc.add(
SelectedFilter(
imagePath: state.filteredImagePath,
),
);
Navigator.pop(context);
},
child: Text(
"Save Changes",
style: TextStyle(
color: Colors.white,
fontSize: 14.sp,
fontWeight: FontWeight.w600,
),
),
),
),
],
),
),
// Processing overlay
if (state.processing == true)
Container(
color: Colors.black.withValues(alpha: .4),
child: const Center(
child: CircularProgressIndicator(
color: Color(0xffF95F62),
),
),
),
],
),
),
);
}
return const Scaffold(
body: SafeArea(child: Center(child: CircularProgressIndicator())),
);
},
);
}
/// Builds a single filter preview thumbnail
Widget buildFilterOption(
BuildContext context,
String label,
File imageFile,
String filter,
bool isSelected,
) {
return GestureDetector(
onTap: () => editImageFilterBloc.add(SelectFilter(filterName: filter)),
child: Container(
margin: const EdgeInsets.only(right: 12),
width: 90,
child: Column(
children: [
ClipRRect(
borderRadius: BorderRadius.circular(10),
child: ColorFiltered(
colorFilter: getColorFilter(filter),
child: Image.file(
imageFile,
height: 70,
width: 90,
fit: BoxFit.cover,
),
),
),
const SizedBox(height: 6),
Text(
label,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
color: isSelected
? const Color(0xffF95F62)
: const Color(0xff2D3134),
),
),
],
),
),
);
}
ColorFilter getColorFilter(String? filter) {
switch (filter) {
case "vintage":
// Muted, warm tones without overflow
return const ColorFilter.matrix([
0.9,
0.3,
0.1,
0,
0,
0.2,
0.8,
0.1,
0,
0,
0.1,
0.3,
0.7,
0,
0,
0,
0,
0,
1,
0,
]);
case "bw":
// Grayscale
return const ColorFilter.matrix([
0.2126,
0.7152,
0.0722,
0,
0,
0.2126,
0.7152,
0.0722,
0,
0,
0.2126,
0.7152,
0.0722,
0,
0,
0,
0,
0,
1,
0,
]);
case "sepia":
// Classic soft brown
return const ColorFilter.matrix([
0.393,
0.769,
0.189,
0,
0,
0.349,
0.686,
0.168,
0,
0,
0.272,
0.534,
0.131,
0,
0,
0,
0,
0,
1,
0,
]);
case "cool":
// Gentle blue tone — no gamma boost to avoid clipping
return const ColorFilter.matrix([
1.0,
0,
0,
0,
0,
0,
1.0,
0,
0,
0,
0,
0,
1.1,
0,
10,
0,
0,
0,
1,
0,
]);
case "contrast":
// Slight contrast increase, safe range
return const ColorFilter.matrix([
1.1,
0,
0,
0,
-10,
0,
1.1,
0,
0,
-10,
0,
0,
1.1,
0,
-10,
0,
0,
0,
1,
0,
]);
case "soft":
// Gentle brightness and warmth — fixed to avoid pixelation
return const ColorFilter.matrix([
1.02,
0,
0,
0,
5,
0,
1.02,
0,
0,
5,
0,
0,
1.02,
0,
5,
0,
0,
0,
1,
0,
]);
default:
return const ColorFilter.mode(Colors.transparent, BlendMode.srcOver);
}
}
}

View File

@@ -1,4 +1,8 @@
import 'dart:io';
import 'package:citycards_customer/postcard/blocs/edit_image_filter/edit_image_filter_bloc.dart';
import 'package:citycards_customer/postcard/blocs/edit_postcard/edit_postcard_bloc.dart';
import 'package:citycards_customer/postcard/blocs/pick_images/pick_images_bloc.dart';
import 'package:citycards_customer/postcard/models/my_postcard_model.dart';
import 'package:citycards_customer/postcard/widgets/dotted_border_container.dart';
import 'package:flutter/material.dart';
@@ -12,6 +16,7 @@ import '../../common_packages/custom_text.dart';
import '../../networkApiServices/api_urls.dart';
import '../widgets/edit_post_card/edit_message.dart';
import '../widgets/edit_post_card/your_details.dart';
import 'edit_image_filter.dart';
class EditPostcardView extends StatefulWidget {
final MyPostCard myPostCard;
@@ -58,6 +63,8 @@ class _EditPostcardViewState extends State<EditPostcardView> {
super.initState();
}
String? selectedImage;
@override
Widget build(BuildContext context) {
final size = MediaQuery.of(context).size;
@@ -122,90 +129,213 @@ class _EditPostcardViewState extends State<EditPostcardView> {
),
),
SizedBox(height: 10.h),
Row(
// crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: CustomPaint(
painter: DottedBorderPainter(),
child: Container(
padding: EdgeInsets.all(10),
height: size.width * 0.45,
width: size.width,
constraints: BoxConstraints(minHeight: 150),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(5),
),
child: ClipRRect(
borderRadius: BorderRadius.circular(10),
child: Image.network(
'${ApiUrls.baseUrl}${postCard!.pcImagePath}',
BlocConsumer<PickImagesBloc, PickImagesState>(
listener: (ctx, state) {
if (state.file != null && state.file!.isNotEmpty) {
setState(() {
selectedImage =
state.filteredFile ?? state.file!;
});
}
},
builder: (context, state) {
return Row(
children: [
Expanded(
child: CustomPaint(
painter: DottedBorderPainter(),
child: Container(
padding: EdgeInsets.all(10),
height: size.width * 0.45,
width: size.width,
fit: BoxFit.cover,
loadingBuilder:
(context, child, loadingProgress) {
if (loadingProgress == null)
return child;
return Container(
height: size.width * 0.45,
width: size.width,
color: Colors.grey[300],
child: const Center(
child:
CircularProgressIndicator(
color: Color(0xffF95F62),
strokeWidth: 2,
),
constraints: BoxConstraints(
minHeight: 150,
),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(5),
),
child: ClipRRect(
borderRadius: BorderRadius.circular(10),
child:
state.file != null &&
state.file!.isNotEmpty
? Image.file(
height: size.width * 0.45,
width: size.width,
fit: BoxFit.cover,
File(
state.filteredFile ??
state.file!,
),
)
: Stack(
children: [
Image.network(
'${ApiUrls.baseUrl}${postCard!.pcImagePath}',
height: size.width * 0.45,
width: size.width,
fit: BoxFit.cover,
loadingBuilder:
(
context,
child,
loadingProgress,
) {
if (loadingProgress ==
null) {
return child;
}
return Container(
height:
size.width *
0.45,
width: size.width,
color: Colors
.grey[300],
child: const Center(
child:
CircularProgressIndicator(
color: Color(
0xffF95F62,
),
strokeWidth:
2,
),
),
);
},
errorBuilder:
(
context,
error,
stackTrace,
) {
return Container(
height:
size.width *
0.45,
width: size.width,
color: Colors
.grey[300],
child: const Icon(
Icons
.image_not_supported,
color:
Colors.grey,
),
);
},
),
Positioned(
child: state.loading == true
? Container(
height:
size.width *
0.45,
width: size.width,
decoration:
BoxDecoration(
color: Colors
.black
.withValues(
alpha:
0.25,
),
),
child: Center(
child: SizedBox(
height: 25,
width: 25,
child: CircularProgressIndicator(
color: Colors
.white,
strokeWidth:
2,
),
),
),
)
: SizedBox(),
),
],
),
);
},
errorBuilder:
(context, error, stackTrace) {
return Container(
height: size.width * 0.45,
width: size.width,
color: Colors.grey[300],
child: const Icon(
Icons.image_not_supported,
color: Colors.grey,
),
);
},
),
),
),
),
),
),
Expanded(
child: Container(
height: size.width * 0.5,
width: size.width,
constraints: BoxConstraints(minHeight: 150),
padding: EdgeInsets.all(10),
child: Column(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
imageButton(
title: 'Take a photo',
icon: Icons.camera_alt_outlined,
width: size.width,
Expanded(
child: Container(
height: size.width * 0.5,
width: size.width,
constraints: BoxConstraints(minHeight: 150),
padding: EdgeInsets.all(10),
child: Column(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
imageButton(
title: 'Take a photo',
icon: Icons.camera_alt_outlined,
width: size.width,
onPressed: () {
context.read<PickImagesBloc>().add(
TakePhoto(),
);
},
),
imageButton(
title: 'Upload Again',
icon: Icons.refresh,
width: size.width,
onPressed: () {
context.read<PickImagesBloc>().add(
PickPhoto(),
);
},
),
imageButton(
title: 'Edit Filters',
width: size.width,
onPressed: () {
final pickImagesBloc = context
.read<PickImagesBloc>();
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => BlocProvider(
create: (context) =>
EditImageFilterBloc(),
child: EditImageFilter(
type:
state.file != null &&
state
.file!
.isNotEmpty
? EditImageType.file
: EditImageType.network,
url:
state.file != null &&
state
.file!
.isNotEmpty
? state.file!
: '${ApiUrls.baseUrl}${postCard!.pcImagePath}',
pickImagesBloc:
pickImagesBloc,
),
),
),
);
},
),
],
),
imageButton(
title: 'Upload Again',
icon: Icons.refresh,
width: size.width,
),
imageButton(
title: 'Edit Filters',
width: size.width,
),
],
),
),
),
),
],
],
);
},
),
SizedBox(height: 10.h),
Text(
@@ -275,7 +405,10 @@ class _EditPostcardViewState extends State<EditPostcardView> {
countryName: _selectedCountry,
);
editPostcardBloc.add(
EditPostCard(myPostCard: postCard!),
EditPostCard(
myPostCard: postCard!,
editImage: selectedImage,
),
);
}
},
@@ -335,7 +468,7 @@ class _EditPostcardViewState extends State<EditPostcardView> {
return SizedBox(
width: width,
child: OutlinedButton(
onPressed: () {},
onPressed: onPressed,
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 14, horizontal: 16),
side: const BorderSide(color: Color(0xffF95F62)),

View File

@@ -1,6 +1,7 @@
import 'dart:developer';
import 'package:citycards_customer/postcard/blocs/edit_postcard/edit_postcard_bloc.dart';
import 'package:citycards_customer/postcard/blocs/pick_images/pick_images_bloc.dart';
import 'package:citycards_customer/postcard/views/edit_postcard_view.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@@ -15,9 +16,22 @@ import '../blocs/myPostCards/my_postcard_event.dart';
import '../blocs/myPostCards/my_postcard_state.dart';
import '../models/my_postcard_model.dart';
class MyPostCardDraftView extends StatelessWidget {
class MyPostCardDraftView extends StatefulWidget {
const MyPostCardDraftView({super.key});
@override
State<MyPostCardDraftView> createState() => _MyPostCardDraftViewState();
}
class _MyPostCardDraftViewState extends State<MyPostCardDraftView> {
final TextEditingController _searchController = TextEditingController();
@override
void dispose() {
_searchController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return BlocBuilder<MyPostCardBloc, MyPostCardState>(
@@ -32,7 +46,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,41 +96,169 @@ 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(
child: SizedBox(
width: 25,
height: 25,
child: CircularProgressIndicator(
color: Color(0XFFF95F62),
),
// 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,
),
)
: SizedBox(),
),
],
),
),
if (state.draftSearchQuery.isNotEmpty) SizedBox(height: 8.h),
// List with Stack for loading
Expanded(
child: Stack(
children: [
RefreshIndicator(
onRefresh: () async {
context.read<MyPostCardBloc>().add(
const RefreshDraftPostCards(),
);
},
color: const Color(0xffF95F62),
child: state.draftPostCards.isEmpty
? ListView(
physics: const AlwaysScrollableScrollPhysics(),
children: [
SizedBox(height: 100.h),
Center(
child: Column(
children: [
Icon(
Icons.search_off,
size: 48,
color: Colors.black26,
),
SizedBox(height: 16.h),
Text(
'No search available',
style: GoogleFonts.poppins(
fontSize: 16.sp,
fontWeight: FontWeight.w500,
color: Colors.black54,
),
),
SizedBox(height: 8.h),
if (state.draftSearchQuery.isNotEmpty)
Padding(
padding: EdgeInsets.symmetric(
horizontal: 32.w,
),
child: Text(
'Try searching with different keywords',
textAlign: TextAlign.center,
style: GoogleFonts.poppins(
fontSize: 14.sp,
color: Colors.black38,
),
),
),
],
),
),
],
)
: ListView.builder(
physics: const AlwaysScrollableScrollPhysics(),
padding: EdgeInsets.symmetric(
horizontal: 12.w,
vertical: 12.h,
),
itemCount: state.draftPostCards.length,
itemBuilder: (context, index) {
final postcard = state.draftPostCards[index];
return _buildDraftCard(context, postcard);
},
),
),
Positioned(
top: 0,
left: 0,
right: 0,
bottom: 0,
child: state.isDeleteLoading == true
? Center(
child: SizedBox(
width: 25,
height: 25,
child: CircularProgressIndicator(
color: Color(0XFFF95F62),
),
),
)
: SizedBox(),
),
],
),
),
],
);
@@ -307,8 +449,16 @@ class MyPostCardDraftView extends StatelessWidget {
onPressed: () async {
final result = await Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => BlocProvider(
create: (context) => EditPostcardBloc(),
builder: (_) => MultiBlocProvider(
providers: [
BlocProvider(
create: (context) => EditPostcardBloc(),
),
BlocProvider(
create: (context) => PickImagesBloc(),
),
],
child: EditPostcardView(myPostCard: postcard),
),
),

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

View File

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

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

@@ -33,9 +33,7 @@ class _EditYourdetailsState extends State<EditYourdetails> {
String? _selectedState;
String? _selectedCountry;
final List<String> countries = [
'Australia',
];
final List<String> countries = ['Australia'];
final List<String> states = [
'New South Wales',
@@ -51,8 +49,12 @@ class _EditYourdetailsState extends State<EditYourdetails> {
@override
void initState() {
setState(() {
_selectedState = widget.selectedState;
_selectedCountry = widget.selectedCountry;
_selectedState = states.contains(widget.selectedState)
? widget.selectedState
: null;
_selectedCountry = countries.contains(widget.selectedCountry)
? widget.selectedCountry
: null;
});
super.initState();
}
@@ -257,10 +259,7 @@ class _EditYourdetailsState extends State<EditYourdetails> {
),
),
items: items.map((String item) {
return DropdownMenuItem<String>(
value: item,
child: Text(item),
);
return DropdownMenuItem<String>(value: item, child: Text(item));
}).toList(),
onChanged: onChanged,
validator: (value) {
@@ -274,4 +273,4 @@ class _EditYourdetailsState extends State<EditYourdetails> {
),
);
}
}
}

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

View File

@@ -830,7 +830,7 @@ packages:
source: hosted
version: "1.9.1"
path_provider:
dependency: transitive
dependency: "direct main"
description:
name: path_provider
sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"

View File

@@ -62,6 +62,7 @@ dependencies:
bloc: ^9.2.0
csc_picker_plus: ^0.0.3
flutter_slidable: ^4.0.3
path_provider: ^2.1.5
dev_dependencies:
flutter_test: