3 Commits

Author SHA1 Message Date
mystery012728
40f0ed3a52 pull taken from shree branch and conflict fixes 2026-02-13 17:13:22 +05:30
Shreeyash Thorat
53264619a8 postcard edit 2026-02-13 15:25:05 +05:30
Shreeyash Thorat
68c3f28d76 itnerary 2026-02-10 15:05:38 +05:30
22 changed files with 1901 additions and 502 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 749 KiB

After

Width:  |  Height:  |  Size: 1.2 MiB

View File

@@ -186,7 +186,7 @@ class AppRouter {
final attractionID = settings.arguments as int;
return MaterialPageRoute(
builder: (_) {
return PassAttractionDetailsView(attractionId: attractionID);
return AttractionDetailsView(attractionId: attractionID);
},
);
@@ -201,9 +201,7 @@ class AppRouter {
final bookingId = settings.arguments as int; // or String
return MaterialPageRoute(
builder: (_) => CheckoutView(
bookingId: bookingId,
),
builder: (_) => CheckoutView(bookingId: bookingId),
);
@@ -239,9 +237,7 @@ class AppRouter {
return MaterialPageRoute(
builder: (_) {
return AddDetailsView(
bookingId: bookingId,
);
return AddDetailsView(bookingId: bookingId);
},
);

View File

@@ -28,7 +28,8 @@ class RouteConstants {
static const String magicItineraryEmptyScreen = '/magicItineraryEmptyScreen';
static const String itineraryCreationStart = '/itineraryCreationStart';
static const String itineraryCreation = '/itineraryCreation';
static const String magicItineraryFilledScreen = "/magicItineraryFilledScreen";
static const String magicItineraryFilledScreen =
"/magicItineraryFilledScreen";
/**************************** ESIM Page *****************************************/
@@ -42,8 +43,8 @@ class RouteConstants {
/**************************** By Pass Page Page *****************************************/
static const String buyPass ='/buyPass';
static const String checkout ='/checkout';
static const String buyPass = '/buyPass';
static const String checkout = '/checkout';
static const String searchOffer = '/searchOffer';
static const String searchPassOffer = '/searchPassOffer';
static const String createAcct = '/createAcct';
@@ -59,4 +60,5 @@ class RouteConstants {
static const String qrPage = '/qrPage';
static const String makeBooking = '/makeBooking';
static const String bookingSuccessful = '/bookingSuccessful';
static const String editPostCard = '/editPostCard';
}

View File

@@ -23,30 +23,47 @@ class CurrentLocationSelection extends StatefulWidget {
class _CurrentLocationSelectionState extends State<CurrentLocationSelection> {
final TextEditingController _controller = TextEditingController();
LatLng? _currentLatLng;
bool loading = false;
Future<void> _getCurrentLocation() async {
LocationPermission permission = await Geolocator.requestPermission();
try {
setState(() {
loading = true;
});
LocationPermission permission = await Geolocator.requestPermission();
if (permission == LocationPermission.denied ||
permission == LocationPermission.deniedForever) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Location permission denied')),
if (permission == LocationPermission.denied ||
permission == LocationPermission.deniedForever) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Location permission denied')),
);
return;
}
final position = await Geolocator.getCurrentPosition(
desiredAccuracy: LocationAccuracy.high,
);
return;
final lat = position.latitude;
final lng = position.longitude;
setState(() {
_currentLatLng = LatLng(lat, lng);
});
await _getAddressFromLatLng(lat, lng);
setState(() {
loading = false;
});
} catch (e) {
setState(() {
loading = false;
});
} finally {
setState(() {
loading = false;
});
}
final position = await Geolocator.getCurrentPosition(
desiredAccuracy: LocationAccuracy.high,
);
final lat = position.latitude;
final lng = position.longitude;
setState(() {
_currentLatLng = LatLng(lat, lng);
});
await _getAddressFromLatLng(lat, lng);
}
Future<void> _getAddressFromLatLng(double lat, double lng) async {
@@ -57,7 +74,6 @@ class _CurrentLocationSelectionState extends State<CurrentLocationSelection> {
final place = placemarks.first;
final address = [
place.name,
place.street,
place.subLocality,
place.locality,
@@ -133,34 +149,41 @@ class _CurrentLocationSelectionState extends State<CurrentLocationSelection> {
child: SizedBox(
height: 250.h,
width: double.infinity,
child: FlutterMap(
options: MapOptions(
initialCenter: _currentLatLng!,
initialZoom: 15,
),
children: [
TileLayer(
urlTemplate:
"https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",
subdomains: const ['a', 'b', 'c'],
userAgentPackageName: 'com.citycards.customer',
),
MarkerLayer(
markers: [
Marker(
point: _currentLatLng!,
width: 40,
height: 40,
child: const Icon(
Icons.location_pin,
color: Colors.red,
size: 40,
),
child: loading == true
? Center(
child: CircularProgressIndicator(
color: Color(0xFFF95F62),
),
],
),
],
),
)
: FlutterMap(
options: MapOptions(
initialCenter: _currentLatLng!,
initialZoom: 15,
),
children: [
TileLayer(
urlTemplate:
"https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",
subdomains: const ['a', 'b', 'c'],
userAgentPackageName:
'com.citycards.customer',
),
MarkerLayer(
markers: [
Marker(
point: _currentLatLng!,
width: 40,
height: 40,
child: const Icon(
Icons.location_pin,
color: Colors.red,
size: 40,
),
),
],
),
],
),
),
)
: GestureDetector(

View File

@@ -1,8 +1,8 @@
class ApiUrls {
// static const baseUrl = "https://devapi.citycards.betadelivery.com";//Normal API
static const baseUrl = "https://testingapi.citycards.betadelivery.com";// Test API
// static const baseUrl = "https://uatapi.citycard.betadelivery.com";// Production Lvl API
// static const baseUrl = "https://testingapi.citycards.betadelivery.com";// Test API
static const baseUrl = "https://uatapi.citycard.betadelivery.com";// Production Lvl API
static const refreshToken = "$baseUrl/auth/refresh";
@@ -25,8 +25,11 @@ class ApiUrls {
static const passDetails = "$baseUrl/mobile/passes";
static const myPassesCart = "$baseUrl/mobile/passes/cart/passes";
static const editPostcard = "$baseUrl/mobile/postcards";
static const myItineraries = "$baseUrl/mobile/itinerary/all-initineraries";
static const getItineraryCities = "$baseUrl/mobile/itinerary/cities-with-icons";
static const getItineraryCities =
"$baseUrl/mobile/itinerary/cities-with-icons";
//Post Apis
static const createAccount = "$baseUrl/mobile/user/register";

View File

@@ -185,6 +185,27 @@ class NetworkApiService {
}
}
// ================= DELETE =================
Future<Response> deleteApi({
required String url,
dynamic data,
Map<String, dynamic>? queryParameters,
Options? options,
CancelToken? cancelToken,
}) async {
try {
return await _dio.delete(
url,
data: data,
queryParameters: queryParameters,
options: options,
cancelToken: cancelToken,
);
} on DioException catch (e) {
throw _handleError(e);
}
}
// ================= REFRESH TOKEN =================
Future<bool> _refreshToken() async {
try {
@@ -225,7 +246,7 @@ class NetworkApiService {
case DioExceptionType.badCertificate:
return "Bad certificate.";
case DioExceptionType.badResponse:
// 🔥 FIXED: Safely handle different response data types
// 🔥 FIXED: Safely handle different response data types
try {
final responseData = error.response?.data;

View File

@@ -0,0 +1,26 @@
import 'dart:developer';
import 'package:bloc/bloc.dart';
import 'package:citycards_customer/postcard/models/my_postcard_model.dart';
import 'package:citycards_customer/postcard/repository/my_postcard_repository.dart';
import 'package:equatable/equatable.dart';
part 'edit_postcard_event.dart';
part 'edit_postcard_state.dart';
class EditPostcardBloc extends Bloc<EditPostcardEvent, EditPostcardState> {
EditPostcardBloc() : super(EditPostcardInitial()) {
on<EditPostCard>((event, emit) async {
try {
emit(EditPostcardLoading());
await MyPostCardsRepository().editMyPostCards(
postcard: event.myPostCard,
);
log("Edit PostCard Successfully");
emit(EditPostcardSuccessfull());
} catch (e) {
emit(EditPostcardError(error: "Failed to edit postcard"));
}
});
}
}

View File

@@ -0,0 +1,13 @@
part of 'edit_postcard_bloc.dart';
class EditPostcardEvent extends Equatable {
const EditPostcardEvent();
@override
List<Object> get props => [];
}
class EditPostCard extends EditPostcardEvent {
final MyPostCard myPostCard;
const EditPostCard({required this.myPostCard});
}

View File

@@ -0,0 +1,19 @@
part of 'edit_postcard_bloc.dart';
class EditPostcardState extends Equatable {
const EditPostcardState();
@override
List<Object> get props => [];
}
class EditPostcardInitial extends EditPostcardState {}
class EditPostcardLoading extends EditPostcardState {}
class EditPostcardSuccessfull extends EditPostcardState {}
class EditPostcardError extends EditPostcardState {
final String error;
const EditPostcardError({required this.error});
}

View File

@@ -1,6 +1,9 @@
import 'dart:developer';
import 'package:citycards_customer/localPreference/local_preference.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'dart:developer' as developer;
import '../../models/my_postcard_model.dart';
import '../../repository/my_postcard_repository.dart';
import 'my_postcard_event.dart';
import 'my_postcard_state.dart';
@@ -8,19 +11,21 @@ import 'my_postcard_state.dart';
class MyPostCardBloc extends Bloc<MyPostCardEvent, MyPostCardState> {
final MyPostCardsRepository repository;
MyPostCardBloc({required this.repository}) : super(const MyPostCardInitial()) {
MyPostCardBloc({required this.repository})
: super(const MyPostCardInitial()) {
on<CheckLoginStatus>(_onCheckLoginStatus);
on<FetchDraftPostCards>(_onFetchDraftPostCards);
on<FetchOrderPostCards>(_onFetchOrderPostCards);
on<RefreshDraftPostCards>(_onRefreshDraftPostCards);
on<RefreshOrderPostCards>(_onRefreshOrderPostCards);
on<DeleteDraftPostCards>(_onDeletePostCard);
}
/// 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());
@@ -29,20 +34,28 @@ class MyPostCardBloc extends Bloc<MyPostCardEvent, MyPostCardState> {
developer.log('📊 Login status: $isLogin', name: 'MyPostCardBloc');
if (isLogin) {
developer.log('✅ User is logged in - initializing state', name: 'MyPostCardBloc');
developer.log(
'✅ User is logged in - initializing state',
name: 'MyPostCardBloc',
);
// User is logged in, initialize with empty lists and loading states
emit(const MyPostCardLoaded(
draftPostCards: [],
orderPostCards: [],
isDraftLoading: true,
isOrderLoading: true,
));
emit(
const MyPostCardLoaded(
draftPostCards: [],
orderPostCards: [],
isDraftLoading: true,
isOrderLoading: true,
),
);
// Fetch both drafts and orders
add(const FetchDraftPostCards());
add(const FetchOrderPostCards());
} else {
developer.log('❌ User is NOT logged in - emitting MyPostCardNotLoggedIn', name: 'MyPostCardBloc');
developer.log(
'❌ User is NOT logged in - emitting MyPostCardNotLoggedIn',
name: 'MyPostCardBloc',
);
// User is not logged in
emit(const MyPostCardNotLoggedIn());
}
@@ -55,9 +68,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;
@@ -69,23 +82,33 @@ class MyPostCardBloc extends Bloc<MyPostCardEvent, MyPostCardState> {
try {
final draftPostCards = await repository.fetchMyPostCards(type: 'draft');
developer.log('✅ Draft postcards fetched: ${draftPostCards.length} items', name: 'MyPostCardBloc');
developer.log(
'✅ Draft postcards fetched: ${draftPostCards.length} items',
name: 'MyPostCardBloc',
);
if (state is MyPostCardLoaded) {
// Update with fetched drafts
emit((state as MyPostCardLoaded).copyWith(
draftPostCards: draftPostCards,
isDraftLoading: false,
));
emit(
(state as MyPostCardLoaded).copyWith(
draftPostCards: draftPostCards,
isDraftLoading: false,
),
);
} else {
developer.log('⚠️ State is not MyPostCardLoaded, creating new state', name: 'MyPostCardBloc');
developer.log(
'⚠️ State is not MyPostCardLoaded, creating new state',
name: 'MyPostCardBloc',
);
// Fallback: create new loaded state (shouldn't normally happen)
emit(MyPostCardLoaded(
draftPostCards: draftPostCards,
orderPostCards: const [],
isDraftLoading: false,
isOrderLoading: false,
));
emit(
MyPostCardLoaded(
draftPostCards: draftPostCards,
orderPostCards: const [],
isDraftLoading: false,
isOrderLoading: false,
),
);
}
} catch (error) {
developer.log('❌ Error fetching drafts: $error', name: 'MyPostCardBloc');
@@ -95,18 +118,37 @@ class MyPostCardBloc extends Bloc<MyPostCardEvent, MyPostCardState> {
}
// Emit error state
emit(MyPostCardError(
errorMessage: error.toString(),
errorType: 'draft',
));
emit(MyPostCardError(errorMessage: error.toString(), errorType: 'draft'));
}
}
Future<void> _onDeletePostCard(
DeleteDraftPostCards event,
Emitter<MyPostCardState> emit,
) async {
if (state is MyPostCardLoaded) {
MyPostCardLoaded currentState = state as MyPostCardLoaded;
try {
emit(currentState.copyWith(isDeleteLoading: true));
await MyPostCardsRepository().deleteMyPostCards(event.id);
List<MyPostCard> items = currentState.draftPostCards;
items.removeWhere((e) => e.id == event.id);
emit(
currentState.copyWith(draftPostCards: items, isDeleteLoading: false),
);
} catch (e) {
log("Erro - $e");
emit(currentState.copyWith(isDeleteLoading: false));
}
}
}
/// 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;
@@ -118,23 +160,33 @@ class MyPostCardBloc extends Bloc<MyPostCardEvent, MyPostCardState> {
try {
final orderPostCards = await repository.fetchMyPostCards(type: 'orders');
developer.log('✅ Order postcards fetched: ${orderPostCards.length} items', name: 'MyPostCardBloc');
developer.log(
'✅ Order postcards fetched: ${orderPostCards.length} items',
name: 'MyPostCardBloc',
);
if (state is MyPostCardLoaded) {
// Update with fetched orders
emit((state as MyPostCardLoaded).copyWith(
orderPostCards: orderPostCards,
isOrderLoading: false,
));
emit(
(state as MyPostCardLoaded).copyWith(
orderPostCards: orderPostCards,
isOrderLoading: false,
),
);
} else {
developer.log('⚠️ State is not MyPostCardLoaded, creating new state', name: 'MyPostCardBloc');
developer.log(
'⚠️ State is not MyPostCardLoaded, creating new state',
name: 'MyPostCardBloc',
);
// Fallback: create new loaded state (shouldn't normally happen)
emit(MyPostCardLoaded(
draftPostCards: const [],
orderPostCards: orderPostCards,
isDraftLoading: false,
isOrderLoading: false,
));
emit(
MyPostCardLoaded(
draftPostCards: const [],
orderPostCards: orderPostCards,
isDraftLoading: false,
isOrderLoading: false,
),
);
}
} catch (error) {
developer.log('❌ Error fetching orders: $error', name: 'MyPostCardBloc');
@@ -144,58 +196,61 @@ class MyPostCardBloc extends Bloc<MyPostCardEvent, MyPostCardState> {
}
// Emit error state
emit(MyPostCardError(
errorMessage: error.toString(),
errorType: 'order',
));
emit(MyPostCardError(errorMessage: error.toString(), errorType: 'order'));
}
}
/// 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');
developer.log('✅ Draft postcards refreshed: ${draftPostCards.length} items', name: 'MyPostCardBloc');
developer.log(
'✅ Draft postcards refreshed: ${draftPostCards.length} items',
name: 'MyPostCardBloc',
);
if (state is MyPostCardLoaded) {
emit((state as MyPostCardLoaded).copyWith(
draftPostCards: draftPostCards,
));
emit(
(state as MyPostCardLoaded).copyWith(draftPostCards: draftPostCards),
);
}
} catch (error) {
developer.log('❌ Error refreshing drafts: $error', name: 'MyPostCardBloc');
emit(MyPostCardError(
errorMessage: error.toString(),
errorType: 'draft',
));
developer.log(
'❌ Error refreshing drafts: $error',
name: 'MyPostCardBloc',
);
emit(MyPostCardError(errorMessage: error.toString(), errorType: 'draft'));
}
}
/// 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');
developer.log('✅ Order postcards refreshed: ${orderPostCards.length} items', name: 'MyPostCardBloc');
developer.log(
'✅ Order postcards refreshed: ${orderPostCards.length} items',
name: 'MyPostCardBloc',
);
if (state is MyPostCardLoaded) {
emit((state as MyPostCardLoaded).copyWith(
orderPostCards: orderPostCards,
));
emit(
(state as MyPostCardLoaded).copyWith(orderPostCards: orderPostCards),
);
}
} catch (error) {
developer.log('❌ Error refreshing orders: $error', name: 'MyPostCardBloc');
emit(MyPostCardError(
errorMessage: error.toString(),
errorType: 'order',
));
developer.log(
'❌ Error refreshing orders: $error',
name: 'MyPostCardBloc',
);
emit(MyPostCardError(errorMessage: error.toString(), errorType: 'order'));
}
}
}
}

View File

@@ -17,6 +17,11 @@ class FetchDraftPostCards extends MyPostCardEvent {
const FetchDraftPostCards();
}
class DeleteDraftPostCards extends MyPostCardEvent {
final int id;
const DeleteDraftPostCards({required this.id});
}
/// Event to fetch order postcards
class FetchOrderPostCards extends MyPostCardEvent {
const FetchOrderPostCards();
@@ -30,4 +35,4 @@ class RefreshDraftPostCards extends MyPostCardEvent {
/// Event to refresh order postcards
class RefreshOrderPostCards extends MyPostCardEvent {
const RefreshOrderPostCards();
}
}

View File

@@ -29,12 +29,14 @@ class MyPostCardLoaded extends MyPostCardState {
final List<MyPostCard> orderPostCards;
final bool isDraftLoading;
final bool isOrderLoading;
final bool isDeleteLoading;
const MyPostCardLoaded({
required this.draftPostCards,
required this.orderPostCards,
this.isDraftLoading = false,
this.isOrderLoading = false,
this.isDeleteLoading = false,
});
@override
@@ -43,6 +45,7 @@ class MyPostCardLoaded extends MyPostCardState {
orderPostCards,
isDraftLoading,
isOrderLoading,
isDeleteLoading,
];
/// Helper method to create a copy with updated values
@@ -51,12 +54,14 @@ class MyPostCardLoaded extends MyPostCardState {
List<MyPostCard>? orderPostCards,
bool? isDraftLoading,
bool? isOrderLoading,
bool? isDeleteLoading,
}) {
return MyPostCardLoaded(
draftPostCards: draftPostCards ?? this.draftPostCards,
orderPostCards: orderPostCards ?? this.orderPostCards,
isDraftLoading: isDraftLoading ?? this.isDraftLoading,
isOrderLoading: isOrderLoading ?? this.isOrderLoading,
isDeleteLoading: isDeleteLoading ?? this.isDeleteLoading,
);
}
}
@@ -66,11 +71,8 @@ class MyPostCardError extends MyPostCardState {
final String errorMessage;
final String errorType; // 'draft' or 'order'
const MyPostCardError({
required this.errorMessage,
required this.errorType,
});
const MyPostCardError({required this.errorMessage, required this.errorType});
@override
List<Object?> get props => [errorMessage, errorType];
}
}

View File

@@ -1,3 +1,4 @@
import 'dart:developer';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../repository/postcard_checkout_repository.dart';
import 'postcard_checkout_event.dart';
@@ -14,62 +15,76 @@ class PostcardCheckoutBloc
on<UpdateCheckoutDataEvent>(_onUpdateCheckoutData);
on<SaveAsDraftEvent>(_onSaveAsDraft);
on<SubmitPostcardEvent>(_onSubmitPostcard);
on<ConfirmPaymentEvent>(_onConfirmPayment); // 🆕 NEW
on<ConfirmPaymentEvent>(_onConfirmPayment);
}
void _onUpdateAddress(
UpdateAddressEvent event, Emitter<PostcardCheckoutState> emit) {
emit(state.copyWith(
countryName: event.countryName,
cityName: event.cityName,
stateName: event.stateName,
zipCode: event.zipCode,
address1: event.address1,
address2: event.address2,
));
UpdateAddressEvent event,
Emitter<PostcardCheckoutState> emit,
) {
emit(
state.copyWith(
countryName: event.countryName,
cityName: event.cityName,
stateName: event.stateName,
zipCode: event.zipCode,
address1: event.address1,
address2: event.address2,
),
);
}
void _onUpdateContent(
UpdatePostcardContentEvent event, Emitter<PostcardCheckoutState> emit) {
emit(state.copyWith(
pcTitle: event.pcTitle,
pcContent: event.pcContent,
pcImageFile: event.pcImageFile,
));
UpdatePostcardContentEvent event,
Emitter<PostcardCheckoutState> emit,
) {
emit(
state.copyWith(
pcTitle: event.pcTitle,
pcContent: event.pcContent,
pcImageFile: event.pcImageFile,
),
);
}
void _onUpdateCheckoutData(
UpdateCheckoutDataEvent event, Emitter<PostcardCheckoutState> emit) {
emit(state.copyWith(
countryName: event.countryName,
cityName: event.cityName,
stateName: event.stateName,
zipCode: event.zipCode,
address1: event.address1,
address2: event.address2,
pcTitle: event.pcTitle,
pcContent: event.pcContent,
pcImageFile: event.pcImageFile,
pcNumber: event.pcNumber,
pcDatetime: event.pcDatetime,
fullname: event.fullname,
emailAddress: event.emailAddress,
mobileNumber: event.mobileNumber,
isdCode: event.isdCode,
isForSelf: event.isForSelf,
baseAmount: event.baseAmount,
totalTaxAmount: event.totalTaxAmount,
totalAmount: event.totalAmount,
postcardId: event.postcardId,
));
UpdateCheckoutDataEvent event,
Emitter<PostcardCheckoutState> emit,
) {
emit(
state.copyWith(
countryName: event.countryName,
cityName: event.cityName,
stateName: event.stateName,
zipCode: event.zipCode,
address1: event.address1,
address2: event.address2,
pcTitle: event.pcTitle,
pcContent: event.pcContent,
pcImageFile: event.pcImageFile,
pcNumber: event.pcNumber,
pcDatetime: event.pcDatetime,
fullname: event.fullname,
emailAddress: event.emailAddress,
mobileNumber: event.mobileNumber,
isdCode: event.isdCode,
isForSelf: event.isForSelf,
baseAmount: event.baseAmount,
totalTaxAmount: event.totalTaxAmount,
totalAmount: event.totalAmount,
postcardId: event.postcardId,
),
);
}
Future<void> _onSaveAsDraft(
SaveAsDraftEvent event, Emitter<PostcardCheckoutState> emit) async {
SaveAsDraftEvent event,
Emitter<PostcardCheckoutState> emit,
) async {
emit(state.copyWith(isLoading: true, error: null, isSuccess: false));
try {
// Validate pcId exists
// Validate pcId exists
if (state.postcardId == null) {
emit(state.copyWith(
isLoading: false,
@@ -79,9 +94,21 @@ class PostcardCheckoutBloc
return;
}
// Validate that image file exists before submitting
if (state.pcImageFile == null) {
emit(
state.copyWith(
isLoading: false,
error: 'Please select a postcard image',
isSuccess: false,
),
);
return;
}
final response = await repository.createPostCard(
pcId: state.postcardId!,
isDraft: true, // ⭐ Save as draft
isDraft: true,
);
// Extract order ID from response if available
@@ -89,27 +116,33 @@ class PostcardCheckoutBloc
response['order_id']?.toString() ??
response['id']?.toString();
emit(state.copyWith(
isLoading: false,
isSuccess: true,
isDraft: true,
orderId: orderId,
));
emit(
state.copyWith(
isLoading: false,
isSuccess: true,
isDraft: true,
orderId: orderId,
),
);
} catch (e) {
emit(state.copyWith(
isLoading: false,
error: e.toString(),
isSuccess: false,
));
emit(
state.copyWith(
isLoading: false,
error: e.toString(),
isSuccess: false,
),
);
}
}
Future<void> _onSubmitPostcard(
SubmitPostcardEvent event, Emitter<PostcardCheckoutState> emit) async {
SubmitPostcardEvent event,
Emitter<PostcardCheckoutState> emit,
) async {
emit(state.copyWith(isLoading: true, error: null, isSuccess: false));
try {
// Validate pcId exists
// Validate pcId exists
if (state.postcardId == null) {
emit(state.copyWith(
isLoading: false,
@@ -119,65 +152,92 @@ class PostcardCheckoutBloc
return;
}
// Validate that image file exists before submitting
if (state.pcImageFile == null) {
emit(
state.copyWith(
isLoading: false,
error: 'Please select a postcard image',
isSuccess: false,
),
);
return;
}
final response = await repository.createPostCard(
pcId: state.postcardId!,
isDraft: false, // ⭐ Initiate payment
isDraft: false,
);
// Parse response from backend
final postcardId = response['postcardId'] as int?;
final clientSecret = response['clientSecret'] as String?;
final status = response['status'] as String?;
// Extract order ID from response
final orderId = response['orderId']?.toString() ??
response['order_id']?.toString() ??
response['id']?.toString();
// Validate clientSecret is present
if (clientSecret == null || clientSecret.isEmpty) {
emit(state.copyWith(
isLoading: false,
error: 'Payment initialization failed - no client secret received',
isSuccess: false,
));
emit(
state.copyWith(
isLoading: false,
error: 'Payment initialization failed - no client secret received',
isSuccess: false,
),
);
return;
}
emit(state.copyWith(
isLoading: false,
isSuccess: true,
isDraft: false,
postcardId: postcardId ?? state.postcardId,
clientSecret: clientSecret,
orderId: orderId,
));
} catch (e) {
emit(state.copyWith(
isLoading: false,
error: e.toString(),
isSuccess: false,
));
// Emit success with clientSecret for payment processing
emit(
state.copyWith(
isLoading: false,
isSuccess: true,
isDraft: false,
postcardId: postcardId ?? state.postcardId,
clientSecret: clientSecret,
orderId: orderId,
),
);
} catch (e, stack) {
log("Payment Error: ${e.toString()}");
log("Payment Stack: ${stack.toString()}");
emit(
state.copyWith(
isLoading: false,
error: e.toString(),
isSuccess: false,
),
);
}
}
/// 🆕 Confirm payment after Stripe payment completes
/// This should be called after Stripe payment succeeds or fails
/// Confirm payment after Stripe payment completes
Future<void> _onConfirmPayment(
ConfirmPaymentEvent event, Emitter<PostcardCheckoutState> emit) async {
ConfirmPaymentEvent event,
Emitter<PostcardCheckoutState> emit,
) async {
// Validate postcardId exists
if (state.postcardId == null) {
emit(state.copyWith(
confirmationError: 'Cannot confirm payment - postcard ID is missing',
isConfirmingPayment: false,
isPaymentConfirmed: false,
));
emit(
state.copyWith(
confirmationError: 'Cannot confirm payment - postcard ID is missing',
isConfirmingPayment: false,
isPaymentConfirmed: false,
),
);
return;
}
emit(state.copyWith(
isConfirmingPayment: true,
confirmationError: null,
isPaymentConfirmed: false,
));
emit(
state.copyWith(
isConfirmingPayment: true,
confirmationError: null,
isPaymentConfirmed: false,
),
);
try {
final response = await repository.confirmPayment(
@@ -187,17 +247,21 @@ class PostcardCheckoutBloc
);
// Payment confirmation successful
emit(state.copyWith(
isConfirmingPayment: false,
isPaymentConfirmed: true,
confirmationError: null,
));
emit(
state.copyWith(
isConfirmingPayment: false,
isPaymentConfirmed: true,
confirmationError: null,
),
);
} catch (e) {
emit(state.copyWith(
isConfirmingPayment: false,
isPaymentConfirmed: false,
confirmationError: e.toString(),
));
emit(
state.copyWith(
isConfirmingPayment: false,
isPaymentConfirmed: false,
confirmationError: e.toString(),
),
);
}
}
}

View File

@@ -170,4 +170,81 @@ class MyPostCard {
'updatedAt': updatedAt.toIso8601String(),
};
}
MyPostCard copyWith({
int? id,
int? userXid,
String? pcTitle,
String? pcNumber,
String? cityName,
DateTime? pcDatetime,
String? pcContent,
String? pcImagePath,
bool? isForSelf,
String? fullname,
String? emailAddress,
String? isdCode,
String? mobileNumber,
String? address1,
String? address2,
String? zipCode,
String? stateName,
String? countryName,
String? orderStatus,
double? baseAmount,
int? couponXid,
double? couponDiscountPercent,
double? couponDiscountAmount,
double? totalTaxAmount,
double? totalAmount,
bool? isPaid,
String? paymentMode,
String? paymentId,
String? paymentStatus,
String? paymentIntentId,
bool? isDraft,
DateTime? deliveredOn,
bool? isActive,
DateTime? createdAt,
DateTime? updatedAt,
}) {
return MyPostCard(
id: id ?? this.id,
userXid: userXid ?? this.userXid,
pcTitle: pcTitle ?? this.pcTitle,
pcNumber: pcNumber ?? this.pcNumber,
cityName: cityName ?? this.cityName,
pcDatetime: pcDatetime ?? this.pcDatetime,
pcContent: pcContent ?? this.pcContent,
pcImagePath: pcImagePath ?? this.pcImagePath,
isForSelf: isForSelf ?? this.isForSelf,
fullname: fullname ?? this.fullname,
emailAddress: emailAddress ?? this.emailAddress,
isdCode: isdCode ?? this.isdCode,
mobileNumber: mobileNumber ?? this.mobileNumber,
address1: address1 ?? this.address1,
address2: address2 ?? this.address2,
zipCode: zipCode ?? this.zipCode,
stateName: stateName ?? this.stateName,
countryName: countryName ?? this.countryName,
orderStatus: orderStatus ?? this.orderStatus,
baseAmount: baseAmount ?? this.baseAmount,
couponXid: couponXid ?? this.couponXid,
couponDiscountPercent:
couponDiscountPercent ?? this.couponDiscountPercent,
couponDiscountAmount: couponDiscountAmount ?? this.couponDiscountAmount,
totalTaxAmount: totalTaxAmount ?? this.totalTaxAmount,
totalAmount: totalAmount ?? this.totalAmount,
isPaid: isPaid ?? this.isPaid,
paymentMode: paymentMode ?? this.paymentMode,
paymentId: paymentId ?? this.paymentId,
paymentStatus: paymentStatus ?? this.paymentStatus,
paymentIntentId: paymentIntentId ?? this.paymentIntentId,
isDraft: isDraft ?? this.isDraft,
deliveredOn: deliveredOn ?? this.deliveredOn,
isActive: isActive ?? this.isActive,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt,
);
}
}

View File

@@ -1,3 +1,7 @@
import 'dart:developer';
import 'package:dio/dio.dart';
import '../../networkApiServices/network_api_services.dart';
import '../../networkApiServices/api_urls.dart';
import '../models/my_postcard_model.dart';
@@ -13,8 +17,61 @@ class MyPostCardsRepository {
url: '${ApiUrls.myPostCards}?type=$type',
);
return (response.data as List)
.map((e) => MyPostCard.fromJson(e))
.toList();
return (response.data as List).map((e) => MyPostCard.fromJson(e)).toList();
}
Future<void> editMyPostCards({required MyPostCard postcard}) async {
try {
final formData = FormData();
formData.fields.addAll([
MapEntry('countryName', postcard.countryName),
MapEntry('cityName', postcard.cityName),
MapEntry('stateName', postcard.stateName),
MapEntry('zipCode', postcard.zipCode),
MapEntry('pcTitle', postcard.pcTitle),
MapEntry('pcContent', postcard.pcContent),
MapEntry('pcNumber', postcard.pcNumber),
MapEntry('pcDatetime', postcard.pcDatetime.toString()),
MapEntry('fullname', postcard.fullname),
MapEntry('isdCode', postcard.isdCode),
]);
if (postcard.address1.isNotEmpty) {
formData.fields.add(MapEntry('address1', postcard.address1));
}
if (postcard.address2.isNotEmpty) {
formData.fields.add(MapEntry('address2', postcard.address2));
}
// final fileName = postcard.pcImagePath.split('/').last;
// formData.files.add(
// MapEntry(
// 'pcImage',
// await MultipartFile.fromFile(
// postcard.pcImagePath,
// filename: fileName,
// ),
// ),
// );
await _apiService.putApi(
url: '${ApiUrls.editPostcard}/${postcard.id}',
data: formData,
);
return;
} catch (e, stack) {
log("Edit PostCard Error - $e");
log("Edit PostCard Error - $stack");
}
}
Future<void> deleteMyPostCards(int id) async {
try {
await _apiService.deleteApi(url: '${ApiUrls.editPostcard}/$id');
return;
} catch (e, stack) {
log("Delete PostCard Error - $e");
log("Delete PostCard Error - $stack");
}
}
}

View File

@@ -0,0 +1,377 @@
import 'package:citycards_customer/postcard/blocs/edit_postcard/edit_postcard_bloc.dart';
import 'package:citycards_customer/postcard/models/my_postcard_model.dart';
import 'package:citycards_customer/postcard/widgets/dotted_border_container.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:google_fonts/google_fonts.dart';
import '../../common_packages/app_bar.dart';
import '../../common_packages/custom_text.dart';
import '../../networkApiServices/api_urls.dart';
import '../widgets/edit_post_card/edit_message.dart';
import '../widgets/edit_post_card/your_details.dart';
class EditPostcardView extends StatefulWidget {
final MyPostCard myPostCard;
const EditPostcardView({super.key, required this.myPostCard});
@override
State<EditPostcardView> createState() => _EditPostcardViewState();
}
class _EditPostcardViewState extends State<EditPostcardView> {
MyPostCard? postCard;
final EditPostcardBloc editPostcardBloc = EditPostcardBloc();
final _formKey = GlobalKey<FormState>();
final _fullNameController = TextEditingController();
final _addressController = TextEditingController();
final _cityController = TextEditingController();
final _zipCodeController = TextEditingController();
String? _selectedCountry;
String? _selectedState;
@override
void dispose() {
_fullNameController.dispose();
_addressController.dispose();
_cityController.dispose();
_zipCodeController.dispose();
super.dispose();
}
@override
void initState() {
setState(() {
postCard = widget.myPostCard;
_fullNameController.text = widget.myPostCard.fullname;
_addressController.text = widget.myPostCard.address1;
_cityController.text = widget.myPostCard.cityName;
_zipCodeController.text = widget.myPostCard.zipCode;
_selectedCountry = widget.myPostCard.countryName;
_selectedState = widget.myPostCard.stateName;
});
super.initState();
}
@override
Widget build(BuildContext context) {
final size = MediaQuery.of(context).size;
return Scaffold(
resizeToAvoidBottomInset: true,
backgroundColor: Colors.white,
body: BlocConsumer<EditPostcardBloc, EditPostcardState>(
bloc: editPostcardBloc,
listener: (ctxx, state) async {
if (state is EditPostcardSuccessfull) {
if (Navigator.canPop(ctxx)) {
Navigator.pop(ctxx, true);
}
} else if (state is EditPostcardError) {
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text(state.error)));
}
},
builder: (context, state) {
return Stack(
children: [
SafeArea(
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 20.w),
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
CommonAppBar(
isWhiteLogo: false,
isProfilePage: false,
showCart: true,
showDivider: true,
),
Row(
children: [
GestureDetector(
onTap: () => Navigator.pop(context),
child: const Icon(Icons.arrow_back),
),
SizedBox(width: 8.w),
CustomText(text: "Edit Postcard", size: 12.sp),
],
),
SizedBox(height: 10.h),
Text(
"Upload Image",
style: GoogleFonts.poppins(
color: Color(0XFF212121),
fontSize: 18.sp,
fontWeight: FontWeight.w500,
),
),
SizedBox(height: 2.h),
Text(
"Edit your own unique postcards by uploading images that capture your unforgettable moments.",
style: GoogleFonts.poppins(
color: Color(0XFF000000).withValues(alpha: 0.6),
fontSize: 14.sp,
fontWeight: FontWeight.w400,
),
),
SizedBox(height: 10.h),
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}',
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,
),
);
},
),
),
),
),
),
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,
),
imageButton(
title: 'Upload Again',
icon: Icons.refresh,
width: size.width,
),
imageButton(
title: 'Edit Filters',
width: size.width,
),
],
),
),
),
],
),
SizedBox(height: 10.h),
Text(
"Edit message",
style: GoogleFonts.poppins(
color: Color(0XFF212121),
fontSize: 18.sp,
fontWeight: FontWeight.w500,
),
),
SizedBox(height: 2.h),
Text(
"Edit your own unique postcards to cherish your unforgettable moments.",
style: GoogleFonts.poppins(
color: Color(0XFF000000).withValues(alpha: 0.6),
fontSize: 14.sp,
fontWeight: FontWeight.w400,
),
),
SizedBox(height: 10.h),
EditMessage(
text: postCard!.pcContent,
onChange: (message, font) {
postCard = postCard!.copyWith(
pcContent: getFormattedMessage(message, font),
);
},
),
SizedBox(height: 10.h),
Form(
key: _formKey,
child: EditYourdetails(
fullNameController: _fullNameController,
addressController: _addressController,
cityController: _cityController,
zipCodeController: _zipCodeController,
selectedCountry: _selectedCountry ?? "",
selectedState: _selectedState ?? "",
formKey: _formKey,
selectState: (String p1) {
setState(() {
_selectedState = p1;
});
},
selectCountry: (String p1) {
setState(() {
_selectedCountry = p1;
});
},
),
),
const SizedBox(height: 30),
// Next Button
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: () {
if (_formKey.currentState!.validate()) {
postCard = postCard!.copyWith(
fullname: _fullNameController.text,
address1: _addressController.text,
cityName: _cityController.text,
zipCode: _zipCodeController.text,
stateName: _selectedState,
countryName: _selectedCountry,
);
editPostcardBloc.add(
EditPostCard(myPostCard: postCard!),
);
}
},
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xffF95F62),
padding: EdgeInsets.symmetric(vertical: 16.h),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(40),
),
),
child: Text(
"Next",
style: TextStyle(
color: Colors.white,
fontSize: 14.sp,
fontWeight: FontWeight.w600,
),
),
),
),
],
),
),
),
),
Positioned(
top: 0,
left: 0,
right: 0,
bottom: 0,
child: state is EditPostcardSuccessfull
? Center(
child: SizedBox(
width: 25,
height: 25,
child: CircularProgressIndicator(
color: Color(0XFFF95F62),
),
),
)
: SizedBox(),
),
],
);
},
),
);
}
Widget imageButton({
Function()? onPressed,
required String title,
IconData? icon,
required double width,
}) {
return SizedBox(
width: width,
child: OutlinedButton(
onPressed: () {},
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 14, horizontal: 16),
side: const BorderSide(color: Color(0xffF95F62)),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(30),
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
title,
style: TextStyle(
color: Color(0xffF95F62),
fontWeight: FontWeight.w500,
fontSize: 12.sp,
),
),
SizedBox(width: icon != null ? 8 : 0),
icon != null ? Icon(icon, color: Color(0xffF95F62)) : SizedBox(),
],
),
),
);
}
String getFormattedMessage(String message, String selectedFont) {
if (message.isEmpty) {
return '';
}
if (selectedFont.isEmpty) {
// Default font (Poppins)
return '<span style="font-family: Poppins;">$message</span>';
}
return '<span style="font-family: $selectedFont;">$message</span>';
}
}

View File

@@ -1,6 +1,11 @@
import 'dart:developer';
import 'package:citycards_customer/postcard/blocs/edit_postcard/edit_postcard_bloc.dart';
import 'package:citycards_customer/postcard/views/edit_postcard_view.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:flutter_slidable/flutter_slidable.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:intl/intl.dart';
import '../../core/route_constants.dart';
@@ -22,9 +27,7 @@ class MyPostCardDraftView extends StatelessWidget {
// Show loading indicator if drafts are loading
if (state.isDraftLoading && state.draftPostCards.isEmpty) {
return const Center(
child: CircularProgressIndicator(
color: Color(0xffF95F62),
),
child: CircularProgressIndicator(color: Color(0xffF95F62)),
);
}
@@ -79,19 +82,43 @@ class MyPostCardDraftView extends StatelessWidget {
}
// Show the list of drafts
return RefreshIndicator(
onRefresh: () async {
context.read<MyPostCardBloc>().add(const RefreshDraftPostCards());
},
color: const Color(0xffF95F62),
child: ListView.builder(
physics: const AlwaysScrollableScrollPhysics(),
itemCount: state.draftPostCards.length,
itemBuilder: (context, index) {
final postcard = state.draftPostCards[index];
return _buildDraftCard(context, postcard);
},
),
return Stack(
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);
},
),
),
Positioned(
top: 0,
left: 0,
right: 0,
bottom: 0,
child: state.isDeleteLoading == true
? Center(
child: SizedBox(
width: 25,
height: 25,
child: CircularProgressIndicator(
color: Color(0XFFF95F62),
),
),
)
: SizedBox(),
),
],
);
}
@@ -130,11 +157,16 @@ class MyPostCardDraftView extends StatelessWidget {
const SizedBox(height: 24),
ElevatedButton(
onPressed: () {
context.read<MyPostCardBloc>().add(const FetchDraftPostCards());
context.read<MyPostCardBloc>().add(
const FetchDraftPostCards(),
);
},
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xffF95F62),
padding: EdgeInsets.symmetric(horizontal: 32.w, vertical: 12.h),
padding: EdgeInsets.symmetric(
horizontal: 32.w,
vertical: 12.h,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(40),
),
@@ -160,131 +192,213 @@ class MyPostCardDraftView extends StatelessWidget {
Widget _buildDraftCard(BuildContext context, MyPostCard postcard) {
return Container(
margin: const EdgeInsets.only(bottom: 16),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: const Color(0xffF95F62).withValues(alpha: 0.08),
borderRadius: BorderRadius.circular(14),
border: Border.all(color: const Color(0xffF1F5F7)),
color: const Color(0xffF95F62),
borderRadius: BorderRadius.circular(10),
border: Border(left: BorderSide(width: 6, color: Color(0XFFF93232))),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
/// LEFT IMAGE
ClipRRect(
borderRadius: BorderRadius.circular(10),
child: Image.network(
'${ApiUrls.baseUrl}${postcard.pcImagePath}',
height: 72,
width: 72,
fit: BoxFit.cover,
loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress == null) return child;
return Container(
height: 72,
width: 72,
color: Colors.grey[300],
child: const Center(
child: CircularProgressIndicator(
color: Color(0xffF95F62),
strokeWidth: 2,
margin: EdgeInsets.only(bottom: 15),
child: Slidable(
key: UniqueKey(),
startActionPane: ActionPane(
motion: const ScrollMotion(),
// dismissible: DismissiblePane(onDismissed: () {}),
children: [
SlidableAction(
onPressed: (ctx) {
context.read<MyPostCardBloc>().add(
DeleteDraftPostCards(id: postcard.id),
);
},
flex: 3,
backgroundColor: Color(0XFFF93232),
foregroundColor: Colors.white,
icon: Icons.delete,
label: 'Delete',
autoClose: true,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(10),
bottomLeft: Radius.circular(10),
),
),
],
),
child: Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(color: const Color(0XFFFFF5F5)),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
/// NUMBER
Text(
"#${postcard.pcNumber}",
style: GoogleFonts.poppins(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Colors.black.withValues(alpha: 0.4),
),
),
const SizedBox(height: 4),
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
/// LEFT IMAGE
ClipRRect(
borderRadius: BorderRadius.circular(10),
child: Image.network(
'${ApiUrls.baseUrl}${postcard.pcImagePath}',
height: 72,
width: 72,
fit: BoxFit.cover,
loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress == null) return child;
return Container(
height: 72,
width: 72,
color: Colors.grey[300],
child: const Center(
child: CircularProgressIndicator(
color: Color(0xffF95F62),
strokeWidth: 2,
),
),
);
},
errorBuilder: (context, error, stackTrace) {
return Container(
height: 72,
width: 72,
color: Colors.grey[300],
child: const Icon(
Icons.image_not_supported,
color: Colors.grey,
),
);
},
),
),
);
},
errorBuilder: (context, error, stackTrace) {
return Container(
height: 72,
width: 72,
color: Colors.grey[300],
child: const Icon(
Icons.image_not_supported,
color: Colors.grey,
const SizedBox(width: 14),
/// RIGHT CONTENT
Expanded(
child: Text(
postcard.pcTitle,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: GoogleFonts.poppins(
fontSize: 15,
fontWeight: FontWeight.w400,
color: Colors.black87,
),
),
),
);
},
),
],
),
const SizedBox(height: 10),
Row(
mainAxisSize: MainAxisSize.min,
children: [
Expanded(
child: ElevatedButton(
onPressed: () async {
final result = await Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => BlocProvider(
create: (context) => EditPostcardBloc(),
child: EditPostcardView(myPostCard: postcard),
),
),
);
if (result == true) {
// ignore: use_build_context_synchronously
context.read<MyPostCardBloc>().add(
const RefreshDraftPostCards(),
);
}
},
style: ElevatedButton.styleFrom(
backgroundColor: Color(
0xfff95f62,
).withValues(alpha: 0.1),
elevation: 0,
padding: EdgeInsets.symmetric(vertical: 16.h),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
side: BorderSide(color: Color(0XFFFDCDCE)),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.edit_outlined,
size: 22,
color: Color(0XFFF95F62),
),
SizedBox(width: 5),
Text(
"Edit",
style: GoogleFonts.poppins(
color: Color(0XFFF95F62),
fontSize: 14.sp,
fontWeight: FontWeight.w600,
),
),
],
),
),
),
SizedBox(width: 4),
Expanded(
child: ElevatedButton(
onPressed: () {},
style: ElevatedButton.styleFrom(
backgroundColor: Color(
0xfff95f62,
).withValues(alpha: 0.1),
elevation: 0,
padding: EdgeInsets.symmetric(vertical: 16.h),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
side: BorderSide(color: Color(0XFFFDCDCE)),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Transform.rotate(
angle: -45,
child: Icon(
Icons.send_outlined,
size: 22,
color: Color(0XFFF95F62),
),
),
SizedBox(width: 5),
Text(
"Send",
style: GoogleFonts.poppins(
color: Color(0XFFF95F62),
fontSize: 14.sp,
fontWeight: FontWeight.w600,
),
),
],
),
),
),
],
),
],
),
const SizedBox(width: 14),
/// RIGHT CONTENT
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
/// NUMBER
Text(
"#${postcard.pcNumber}",
style: GoogleFonts.poppins(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Colors.black,
),
),
const SizedBox(height: 4),
/// TITLE
Text(
postcard.pcTitle,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: GoogleFonts.poppins(
fontSize: 15,
fontWeight: FontWeight.w400,
color: Colors.black87,
),
),
const SizedBox(height: 10),
/// ICONS BOTTOM RIGHT (UNDER TITLE)
Align(
alignment: Alignment.centerRight,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
GestureDetector(
onTap: () {
// delete
},
child: Image.asset(
'assets/icons/delete_icon.png',
width: 20,
height: 20,
),
),
const SizedBox(width: 16),
GestureDetector(
onTap: () {
// edit
},
child: Image.asset(
'assets/icons/edit_icon.png',
width: 20,
height: 20,
),
),
const SizedBox(width: 16),
GestureDetector(
onTap: () {
// send
},
child: Image.asset(
'assets/icons/send_icon.png',
width: 20,
height: 20,
),
),
],
),
),
],
),
),
],
),
),
);
}
}
}

View File

@@ -23,9 +23,7 @@ class MyPostCardOrdersView extends StatelessWidget {
// Show loading indicator if orders are loading
if (state.isOrderLoading && state.orderPostCards.isEmpty) {
return const Center(
child: CircularProgressIndicator(
color: Color(0xffF95F62),
),
child: CircularProgressIndicator(color: Color(0xffF95F62)),
);
}
@@ -131,11 +129,16 @@ class MyPostCardOrdersView extends StatelessWidget {
const SizedBox(height: 24),
ElevatedButton(
onPressed: () {
context.read<MyPostCardBloc>().add(const FetchOrderPostCards());
context.read<MyPostCardBloc>().add(
const FetchOrderPostCards(),
);
},
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xffF95F62),
padding: EdgeInsets.symmetric(horizontal: 32.w, vertical: 12.h),
padding: EdgeInsets.symmetric(
horizontal: 32.w,
vertical: 12.h,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(40),
),
@@ -160,41 +163,71 @@ class MyPostCardOrdersView extends StatelessWidget {
}
Widget _buildOrderCard(BuildContext context, MyPostCard postcard) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Postcard Number above the card
Padding(
padding: const EdgeInsets.only(left: 4, bottom: 8),
child: Text(
"#${postcard.pcNumber}",
style: GoogleFonts.poppins(
color: Colors.black,
fontWeight: FontWeight.w500,
fontSize: 15.sp,
),
return Container(
margin: const EdgeInsets.only(bottom: 16),
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: const Color(0xffF95F62).withValues(alpha: 0.08),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: const Color(0xffF1F5F7)),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
children: [
Text(
"#${postcard.pcNumber}",
style: GoogleFonts.poppins(
color: Colors.black.withValues(alpha: 0.4),
fontWeight: FontWeight.w400,
fontSize: 12.sp,
),
),
SizedBox(width: 10),
Spacer(),
Text(
"Status:",
style: GoogleFonts.poppins(
color: Colors.black,
fontWeight: FontWeight.w400,
fontSize: 10.sp,
),
),
SizedBox(width: 5),
Container(
padding: const EdgeInsets.fromLTRB(13, 7, 13, 7),
decoration: BoxDecoration(
color: _getStatusColor(
postcard.orderStatus,
).withOpacity(0.16),
border: Border.all(
color: _getStatusBorderColor(postcard.orderStatus),
),
borderRadius: BorderRadius.circular(16),
),
child: Text(
_getStatusText(postcard.orderStatus),
style: TextStyle(
color: Colors.black,
fontWeight: FontWeight.w400,
fontSize: 8.54.sp,
),
),
),
],
),
),
// Order Card
Container(
margin: const EdgeInsets.only(bottom: 16),
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: const Color(0xffF95F62).withValues(alpha:0.08),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: const Color(0xffF1F5F7),
),
),
child: Row(
SizedBox(width: 10),
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
// Postcard Image
ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image(
image: NetworkImage('${ApiUrls.baseUrl}${postcard.pcImagePath}'),
image: NetworkImage(
'${ApiUrls.baseUrl}${postcard.pcImagePath}',
),
height: 70.h,
width: 70.w,
fit: BoxFit.cover,
@@ -233,98 +266,67 @@ class MyPostCardOrdersView extends StatelessWidget {
// Postcard Details
Expanded(
child: SizedBox(
child: Container(
alignment: Alignment.centerLeft,
height: 60.h,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: [
Text(
postcard.pcTitle,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: GoogleFonts.poppins(
color: Colors.black,
fontWeight: FontWeight.w400,
fontSize: 16.sp,
),
),
const SizedBox(height: 6),
Text(
"5 Post cards",
style: GoogleFonts.poppins(
color: Colors.black,
fontWeight: FontWeight.w400,
fontSize: 14.sp,
),
),
],
),
Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Container(
padding: const EdgeInsets.fromLTRB(13, 7, 13, 7),
decoration: BoxDecoration(
color: _getStatusColor(postcard.orderStatus).withOpacity(0.16),
border: Border.all(
color: _getStatusBorderColor(postcard.orderStatus),
),
borderRadius: BorderRadius.circular(16),
),
child: Text(
_getStatusText(postcard.orderStatus),
style: TextStyle(
color: Colors.black,
fontWeight: FontWeight.w400,
fontSize: 8.54.sp,
),
),
),
InkWell(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => MyPostcardPreviewView(
postcard: postcard,
),
),
);
},
child: Row(
children: [
Icon(
Icons.remove_red_eye_outlined,
size: 15,
color: const Color(0xffF95F62),
),
SizedBox(width: 5.w),
Text(
"Preview",
style: TextStyle(
fontWeight: FontWeight.w400,
color: const Color(0xffF95F62),
fontSize: 13.sp,
),
),
],
),
),
],
),
],
child: Text(
postcard.pcTitle,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: GoogleFonts.poppins(
color: Colors.black,
fontWeight: FontWeight.w400,
fontSize: 16.sp,
),
),
),
),
],
),
),
],
Container(
margin: EdgeInsets.only(top: 10),
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Color(0xfff95f62).withValues(alpha: 0.1),
elevation: 0,
padding: EdgeInsets.symmetric(vertical: 16.h),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
side: BorderSide(color: Color(0XFFFDCDCE)),
),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
MyPostcardPreviewView(postcard: postcard),
),
);
},
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.remove_red_eye_outlined,
size: 15,
color: const Color(0xffF95F62),
),
SizedBox(width: 5.w),
Text(
"Preview",
style: TextStyle(
fontWeight: FontWeight.w400,
color: const Color(0xffF95F62),
fontSize: 13.sp,
),
),
],
),
),
),
],
),
);
}
@@ -382,4 +384,4 @@ class MyPostCardOrdersView extends StatelessWidget {
return status;
}
}
}
}

View File

@@ -0,0 +1,257 @@
import 'package:citycards_customer/postcard/views/write_message_step_page_view.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:html/parser.dart' as html_parser;
class EditMessage extends StatefulWidget {
final String text;
final Function(String, String) onChange;
const EditMessage({super.key, required this.text, required this.onChange});
@override
State<EditMessage> createState() => _EditMessageState();
}
class _EditMessageState extends State<EditMessage> {
final TextEditingController _controller = TextEditingController();
final fonts = [
{"name": "Default", "font": GoogleFonts.poppins(), "cleanName": "Poppins"},
{
"name": "Patrick Hand",
"font": GoogleFonts.patrickHand(),
"cleanName": "Patrick Hand",
},
{
"name": "Indie Flower",
"font": GoogleFonts.indieFlower(),
"cleanName": "Indie Flower",
},
{
"name": "Gloria Hallelujah",
"font": GoogleFonts.gloriaHallelujah(),
"cleanName": "Gloria Hallelujah",
},
];
String selectedFont = "Poppins";
@override
void initState() {
final parsedMessage = _parseHtmlMessage(widget.text);
final messageText = parsedMessage['text'] ?? '';
final fontFamily = parsedMessage['fontFamily'] ?? '';
setState(() {
_controller.text = messageText;
selectedFont = fontFamily;
});
super.initState();
}
@override
Widget build(BuildContext context) {
return Column(
children: [
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: const Color(0xFFFFF5F5),
borderRadius: BorderRadius.circular(12),
),
child: CustomPaint(
painter: LinedPaperPainter(),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: TextField(
controller: _controller,
maxLines: 8,
maxLength: 400,
cursorColor: const Color(0xffF95F62),
style: _getTextFieldStyle(selectedFont, fonts),
decoration: InputDecoration(
border: InputBorder.none,
hintText: "Add Your Message Here",
hintStyle: TextStyle(
color: const Color(0xff999999),
fontSize: 14.sp,
),
counterText: "",
),
onChanged: (val) {
widget.onChange(val, selectedFont);
},
),
),
),
),
Align(
alignment: Alignment.bottomRight,
child: Padding(
padding: const EdgeInsets.only(top: 6, right: 8),
child: Text(
"${_controller.text.length}/400",
style: TextStyle(fontSize: 12.sp, color: const Color(0xff999999)),
),
),
),
const SizedBox(height: 20),
SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: fonts.map((font) {
final TextStyle fontStyle = font['font'] as TextStyle;
final String fontName = font["name"] as String;
final String cleanName = font["cleanName"] as String;
final isSelected = selectedFont == cleanName;
return GestureDetector(
onTap: () {
setState(() {
selectedFont = cleanName;
});
widget.onChange(_controller.text, selectedFont);
},
child: Container(
padding: const EdgeInsets.all(6),
width: 100.w,
height: 100.h,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
),
child: CustomPaint(
painter: DottedBorderPainter(
color: isSelected
? const Color(0xffF95F62)
: const Color(0xffE0E0E0),
strokeWidth: 1.5,
dashWidth: 4,
dashSpace: 3,
borderRadius: 12,
),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
"Aa",
style: fontStyle.copyWith(
fontSize: 24.sp,
color: const Color(0xff1A1A1A),
),
),
const SizedBox(height: 4),
Text(
fontName,
textAlign: TextAlign.center,
style: fontStyle.copyWith(
fontSize: 11.sp,
color: isSelected
? const Color(0xffF95F62)
: const Color(0xff2D3134),
),
),
],
),
),
),
),
);
}).toList(),
),
),
],
);
}
TextStyle _getTextFieldStyle(
String? selectedFont,
List<Map<String, dynamic>> fonts,
) {
if (selectedFont == null || selectedFont.isEmpty) {
return GoogleFonts.poppins(fontSize: 14.sp, color: Colors.black);
}
// Find matching font by cleanName
for (var font in fonts) {
if (font['cleanName'] == selectedFont) {
final TextStyle fontStyle = font['font'] as TextStyle;
return fontStyle.copyWith(fontSize: 14.sp, color: Colors.black);
}
}
// Default fallback to Poppins
return GoogleFonts.poppins(fontSize: 14.sp, color: Colors.black);
}
Map<String, String> _parseHtmlMessage(String htmlMessage) {
if (htmlMessage.isEmpty) {
return {'text': '', 'fontFamily': ''};
}
// Check if message contains HTML tags
if (!htmlMessage.contains('<span') && !htmlMessage.contains('style=')) {
// Plain text message - no font specified
return {'text': htmlMessage, 'fontFamily': ''};
}
try {
// Parse HTML
final document = html_parser.parse(htmlMessage);
final spanElement = document.querySelector('span');
if (spanElement != null) {
// Extract text content
final text = spanElement.text;
// Extract font-family from style attribute
final style = spanElement.attributes['style'] ?? '';
final fontFamilyMatch = RegExp(
r'font-family:\s*([^;]+)',
).firstMatch(style);
final fontFamily = fontFamilyMatch?.group(1)?.trim() ?? '';
return {'text': text, 'fontFamily': fontFamily};
}
// Fallback: return plain text
return {'text': document.body?.text ?? htmlMessage, 'fontFamily': ''};
} catch (e) {
// If parsing fails, return original message
return {'text': htmlMessage, 'fontFamily': ''};
}
}
// Get TextStyle with any Google Font
TextStyle _getFontStyle(String fontFamily, double fontSize, double height) {
// If no font family specified, use default Caveat
if (fontFamily.isEmpty) {
return GoogleFonts.caveat(fontSize: fontSize, height: height);
}
try {
// Normalize font name: remove extra spaces, handle common variations
final normalizedFont = fontFamily.trim().replaceAll(
RegExp(r'\s+'),
' ',
); // Replace multiple spaces with single space
// Try to get the font from Google Fonts
// GoogleFonts.getFont() can load ANY Google Font dynamically
return GoogleFonts.getFont(
normalizedFont,
fontSize: fontSize,
height: height,
);
} catch (e) {
// If font not found in Google Fonts, fallback to default
debugPrint(
'⚠️ Font "$fontFamily" not found in Google Fonts. Using default Caveat font.',
);
return GoogleFonts.caveat(fontSize: fontSize, height: height);
}
}
}

View File

@@ -0,0 +1,277 @@
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:google_fonts/google_fonts.dart';
class EditYourdetails extends StatefulWidget {
final TextEditingController fullNameController;
final TextEditingController addressController;
final TextEditingController cityController;
final TextEditingController zipCodeController;
final String selectedCountry;
final String selectedState;
final GlobalKey<FormState> formKey;
final Function(String) selectState;
final Function(String) selectCountry;
const EditYourdetails({
super.key,
required this.fullNameController,
required this.addressController,
required this.cityController,
required this.zipCodeController,
required this.selectedCountry,
required this.selectedState,
required this.formKey,
required this.selectState,
required this.selectCountry,
});
@override
State<EditYourdetails> createState() => _EditYourdetailsState();
}
class _EditYourdetailsState extends State<EditYourdetails> {
String? _selectedState;
String? _selectedCountry;
final List<String> countries = [
'Australia',
];
final List<String> states = [
'New South Wales',
'Victoria',
'Queensland',
'South Australia',
'Western Australia',
'Tasmania',
'Northern Territory',
'Australian Capital Territory',
];
@override
void initState() {
setState(() {
_selectedState = widget.selectedState;
_selectedCountry = widget.selectedCountry;
});
super.initState();
}
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Recipient Details",
style: GoogleFonts.poppins(
color: Color(0XFF212121),
fontSize: 18.sp,
fontWeight: FontWeight.w500,
),
),
SizedBox(height: 2.h),
Text(
"Enter the address of the person who will receive this postcard",
style: GoogleFonts.poppins(
color: Color(0XFF000000).withValues(alpha: 0.6),
fontSize: 14.sp,
fontWeight: FontWeight.w400,
),
),
const SizedBox(height: 16),
_buildInputField(
label: "Recipient",
hint: "Enter the recipient's name",
controller: widget.fullNameController,
),
_buildInputField(
label: "Address",
hint: "Enter the recipient's Address",
controller: widget.addressController,
),
_buildInputField(
label: "City",
hint: "Enter the name of your city",
controller: widget.cityController,
),
_buildDropdownField(
label: "Country",
hint: "Select your country",
value: _selectedCountry,
items: countries,
onChanged: (val) {
setState(() {
_selectedCountry = val;
});
widget.selectCountry(val!);
},
),
_buildDropdownField(
label: "State",
hint: "Select your state",
value: _selectedState,
items: states,
onChanged: (val) {
setState(() {
_selectedState = val;
});
widget.selectState(val!);
},
),
_buildInputField(
label: "Zip Code",
hint: "Enter the Zip Code you reside in",
controller: widget.zipCodeController,
keyboardType: TextInputType.number,
),
],
);
}
Widget _buildInputField({
required String label,
required String hint,
required TextEditingController controller,
IconData? icon,
TextInputType? keyboardType,
}) {
return Padding(
padding: const EdgeInsets.only(bottom: 18),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: GoogleFonts.poppins(
fontSize: 13.sp,
fontWeight: FontWeight.w500,
color: const Color(0xff1A1A1A),
),
),
const SizedBox(height: 6),
TextFormField(
controller: controller,
keyboardType: keyboardType,
decoration: InputDecoration(
hintText: hint,
hintStyle: GoogleFonts.poppins(
color: const Color(0xff999999),
fontSize: 14.sp,
),
suffixIcon: icon != null
? Icon(icon, color: Colors.black, size: 20)
: null,
contentPadding: const EdgeInsets.symmetric(
vertical: 14,
horizontal: 12,
),
enabledBorder: OutlineInputBorder(
borderSide: const BorderSide(color: Color(0xffFDCDCE)),
borderRadius: BorderRadius.circular(8),
),
focusedBorder: OutlineInputBorder(
borderSide: const BorderSide(color: Color(0xffFDCDCE)),
borderRadius: BorderRadius.circular(8),
),
errorBorder: OutlineInputBorder(
borderSide: const BorderSide(color: Colors.red),
borderRadius: BorderRadius.circular(8),
),
focusedErrorBorder: OutlineInputBorder(
borderSide: const BorderSide(color: Colors.red),
borderRadius: BorderRadius.circular(8),
),
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter $label';
}
if (label == "Email ID" && !value.contains('@')) {
return 'Please enter a valid email';
}
return null;
},
),
],
),
);
}
Widget _buildDropdownField({
required String label,
required String hint,
required String? value,
required List<String> items,
required Function(String?) onChanged,
}) {
return Padding(
padding: const EdgeInsets.only(bottom: 18),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: GoogleFonts.poppins(
fontSize: 13.sp,
fontWeight: FontWeight.w500,
color: const Color(0xff1A1A1A),
),
),
const SizedBox(height: 6),
DropdownButtonFormField<String>(
value: value,
decoration: InputDecoration(
contentPadding: const EdgeInsets.symmetric(
vertical: 14,
horizontal: 12,
),
enabledBorder: OutlineInputBorder(
borderSide: const BorderSide(color: Color(0xffFDCDCE)),
borderRadius: BorderRadius.circular(8),
),
focusedBorder: OutlineInputBorder(
borderSide: const BorderSide(color: Color(0xffFDCDCE)),
borderRadius: BorderRadius.circular(8),
),
errorBorder: OutlineInputBorder(
borderSide: const BorderSide(color: Colors.red),
borderRadius: BorderRadius.circular(8),
),
focusedErrorBorder: OutlineInputBorder(
borderSide: const BorderSide(color: Colors.red),
borderRadius: BorderRadius.circular(8),
),
),
icon: const Icon(
Icons.keyboard_arrow_down,
color: Color(0xffFDCDCE),
),
hint: Text(
hint,
style: GoogleFonts.poppins(
color: const Color(0xff999999),
fontSize: 14.sp,
),
),
items: items.map((String item) {
return DropdownMenuItem<String>(
value: item,
child: Text(item),
);
}).toList(),
onChanged: onChanged,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please select $label';
}
return null;
},
),
],
),
);
}
}

View File

@@ -371,6 +371,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "5.9.3"
flutter_slidable:
dependency: "direct main"
description:
name: flutter_slidable
sha256: ea369262929d3cc6ebf9d8a00c196127966f117fe433a5e5cb47fb08008ca203
url: "https://pub.dev"
source: hosted
version: "4.0.3"
flutter_stripe:
dependency: "direct main"
description:

View File

@@ -61,6 +61,7 @@ dependencies:
cached_network_image: ^3.4.1
bloc: ^9.2.0
csc_picker_plus: ^0.0.3
flutter_slidable: ^4.0.3
dev_dependencies:
flutter_test: