last 16-10-2025
This commit is contained in:
commit
5fdfdd9302
146 files changed
+10525
No files matched your search
@@ -0,0 +1,21 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:wordpress/view/home_screen.dart';
|
||||
import 'package:wordpress/view/main_controller.dart';
|
||||
|
||||
void main() {
|
||||
runApp(const ProviderScope(child: WordpressApp()));
|
||||
}
|
||||
|
||||
class WordpressApp extends StatelessWidget {
|
||||
const WordpressApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const GetMaterialApp(
|
||||
debugShowCheckedModeBanner: false,
|
||||
home: MainController(), // Initial screen
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
class ConstsApi {
|
||||
static const String baseUrl = "https://project2022.amrithaa.com";
|
||||
|
||||
// Example endpoints
|
||||
static const String home = "$baseUrl/ecomdemo/wp-json/homeapi/v1/data";
|
||||
static const String allproduct = "$baseUrl/ecomdemo/wp-json/wc/v3/products";
|
||||
static const String cart =
|
||||
"$baseUrl/ecomdemo/wp-json/custom-cart/v1/view?cart_token=cart_66fa27d8d44e4";
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:wordpress/providers/api_const.dart';
|
||||
import 'package:wordpress/providers/api_repositoryprovider.dart';
|
||||
import 'package:wordpress/viewmodel/all_product_model.dart';
|
||||
import 'package:wordpress/viewmodel/cart_model.dart';
|
||||
import 'package:wordpress/viewmodel/detail_model.dart';
|
||||
import 'package:wordpress/viewmodel/product_home_page_model.dart';
|
||||
|
||||
final apiRepositoryProvider = Provider((ref) => ApiRepository());
|
||||
|
||||
final productHomePageProvider =
|
||||
FutureProvider.autoDispose<ProductHomePageModel>((ref) async {
|
||||
final repo = ref.read(apiRepositoryProvider);
|
||||
return repo.fetchHomePage(ConstsApi.home);
|
||||
});
|
||||
final allProductProvider = FutureProvider.autoDispose<List<AllProductModel>>((
|
||||
ref,
|
||||
) async {
|
||||
final repo = ref.read(apiRepositoryProvider);
|
||||
return repo.fetchAllProducts(ConstsApi.allproduct);
|
||||
});
|
||||
|
||||
final cartProvider = FutureProvider.autoDispose<CartResponse>((ref) async {
|
||||
final repo = ref.read(apiRepositoryProvider);
|
||||
return repo.fetchCart(ConstsApi.cart);
|
||||
});
|
||||
|
||||
final productDetailProvider = FutureProvider.autoDispose
|
||||
.family<DetailModel, int>((ref, id) async {
|
||||
final repo = ref.read(apiRepositoryProvider);
|
||||
return repo.fetchProductDetail(id);
|
||||
});
|
||||
final cartHelperProvider = Provider((ref) => WooCartHelper());
|
||||
|
||||
final cartIdProvider = StateProvider<int?>((ref) => null);
|
||||
|
||||
final cartItemsProvider = FutureProvider<List<CartItem>>((ref) async {
|
||||
final cartId = ref.watch(cartIdProvider);
|
||||
if (cartId == null) return [];
|
||||
|
||||
final cartHelper = ref.read(cartHelperProvider);
|
||||
return await cartHelper.getCartItems();
|
||||
});
|
||||
|
||||
final cartItemCountProvider = Provider<int>((ref) {
|
||||
final cartItems = ref.watch(cartItemsProvider);
|
||||
return cartItems.when(
|
||||
data: (items) => items.fold(0, (sum, item) => sum + item.quantity),
|
||||
loading: () => 0,
|
||||
error: (_, __) => 0,
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,283 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:wordpress/providers/api_const.dart';
|
||||
import 'package:wordpress/viewmodel/all_product_model.dart';
|
||||
import 'package:wordpress/viewmodel/cart_model.dart';
|
||||
import 'package:wordpress/viewmodel/detail_model.dart';
|
||||
import 'package:wordpress/viewmodel/product_home_page_model.dart';
|
||||
|
||||
class ApiRepository {
|
||||
static const String cartToken = "cart_66fa27d8d44e4";
|
||||
static const String secretKey = "my_secret_key_ecom2025";
|
||||
static const String baseUrl =
|
||||
"https://project2022.amrithaa.com/ecomdemo/wp-json/custom-cart/v1";
|
||||
|
||||
// Fetch homepage data
|
||||
Future<ProductHomePageModel> fetchHomePage(String url) async {
|
||||
final response = await http.get(
|
||||
Uri.parse(url),
|
||||
headers: {
|
||||
'x-api-key': 's8f7g6h5j4k3l2m1n0p9q8r7s6t5u4v3',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final Map<String, dynamic> decoded = json.decode(response.body);
|
||||
final Map<String, dynamic> jsonData = decoded.containsKey('data')
|
||||
? decoded['data']
|
||||
: decoded;
|
||||
return ProductHomePageModel.fromJson(jsonData);
|
||||
} else {
|
||||
throw Exception('Failed to load home page data: ${response.statusCode}');
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<AllProductModel>> fetchAllProducts(String url) async {
|
||||
final uri = Uri.parse(url).replace(
|
||||
queryParameters: {
|
||||
'consumer_key': 'ck_f9a199c7fdeb13e3c052578361f4d238c6199fec',
|
||||
'consumer_secret': 'cs_50688e7371ca6071a9d60ceb862477522b4e3b84',
|
||||
},
|
||||
);
|
||||
|
||||
final response = await http.get(
|
||||
uri,
|
||||
headers: {
|
||||
'x-api-key': 's8f7g6h5j4k3l2m1n0p9q8r7s6t5u4v3',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final List decoded = json.decode(response.body);
|
||||
return decoded.map((json) => AllProductModel.fromJson(json)).toList();
|
||||
} else {
|
||||
throw Exception('Failed to load products: ${response.statusCode}');
|
||||
}
|
||||
}
|
||||
|
||||
Future<CartResponse> fetchCart(String url) async {
|
||||
final uri = Uri.parse(url);
|
||||
|
||||
final response = await http.get(
|
||||
uri,
|
||||
headers: {'X-CART-SECRET': secretKey, 'Content-Type': 'application/json'},
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final Map<String, dynamic> decoded = json.decode(response.body);
|
||||
return CartResponse.fromJson(decoded);
|
||||
} else {
|
||||
throw Exception('Failed to load cart: ${response.statusCode}');
|
||||
}
|
||||
}
|
||||
|
||||
Future<DetailModel> fetchProductDetail(int id) async {
|
||||
final uri = Uri.parse("${ConstsApi.allproduct}/$id").replace(
|
||||
queryParameters: {
|
||||
'consumer_key': 'ck_f9a199c7fdeb13e3c052578361f4d238c6199fec',
|
||||
'consumer_secret': 'cs_50688e7371ca6071a9d60ceb862477522b4e3b84',
|
||||
},
|
||||
);
|
||||
|
||||
final response = await http.get(
|
||||
uri,
|
||||
headers: {
|
||||
'x-api-key': 's8f7g6h5j4k3l2m1n0p9q8r7s6t5u4v3',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final decoded = json.decode(response.body);
|
||||
return DetailModel.fromJson(decoded);
|
||||
} else {
|
||||
throw Exception('Failed to load product detail: ${response.statusCode}');
|
||||
}
|
||||
}
|
||||
|
||||
/// ✅ Add Item
|
||||
Future<void> addItem({required int productId, required int quantity}) async {
|
||||
final url = Uri.parse("$baseUrl/add");
|
||||
|
||||
final body = {
|
||||
"cart_token": cartToken,
|
||||
"product_id": productId,
|
||||
"quantity": quantity,
|
||||
};
|
||||
|
||||
final headers = {
|
||||
HttpHeaders.contentTypeHeader: 'application/json',
|
||||
'X-CART-SECRET': secretKey,
|
||||
};
|
||||
|
||||
print('🔗 Add URL: $url');
|
||||
print('📦 Add Body: $body');
|
||||
|
||||
try {
|
||||
final response = await http.post(
|
||||
url,
|
||||
headers: headers,
|
||||
body: jsonEncode(body),
|
||||
);
|
||||
print('📡 Add Status: ${response.statusCode}');
|
||||
print('📡 Add Body: ${response.body}');
|
||||
} catch (e) {
|
||||
print('💥 Add Exception: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// ✅ Remove Item
|
||||
Future<void> removeItem({
|
||||
required int productId,
|
||||
required int quantity,
|
||||
}) async {
|
||||
final url = Uri.parse("$baseUrl/remove");
|
||||
|
||||
final body = {
|
||||
"cart_token": cartToken,
|
||||
"product_id": productId,
|
||||
"quantity": quantity,
|
||||
};
|
||||
|
||||
final headers = {
|
||||
HttpHeaders.contentTypeHeader: 'application/json',
|
||||
'X-CART-SECRET': secretKey,
|
||||
};
|
||||
|
||||
print('🔗 Remove URL: $url');
|
||||
print('📦 Remove Body: $body');
|
||||
|
||||
try {
|
||||
final response = await http.post(
|
||||
url,
|
||||
headers: headers,
|
||||
body: jsonEncode(body),
|
||||
);
|
||||
print('📡 Remove Status: ${response.statusCode}');
|
||||
print('📡 Remove Body: ${response.body}');
|
||||
} catch (e) {
|
||||
print('💥 Remove Exception: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class WooCartHelper {
|
||||
final String baseUrl =
|
||||
'https://project2022.amrithaa.com/ecomdemo/wp-json/wc/v3';
|
||||
final String consumerKey = 'ck_f9a199c7fdeb13e3c052578361f4d238c6199fec';
|
||||
final String consumerSecret = 'cs_50688e7371ca6071a9d60ceb862477522b4e3b84';
|
||||
|
||||
// Helper: build URL with credentials
|
||||
Uri buildUrl(String endpoint, [Map<String, String>? extraParams]) {
|
||||
final params = {
|
||||
'consumer_key': consumerKey,
|
||||
'consumer_secret': consumerSecret,
|
||||
...?extraParams,
|
||||
};
|
||||
return Uri.parse('$baseUrl$endpoint').replace(queryParameters: params);
|
||||
}
|
||||
|
||||
// Create a new cart (pending order)
|
||||
Future<int> createCart() async {
|
||||
final url = buildUrl('/orders');
|
||||
final response = await http.post(
|
||||
url,
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: jsonEncode({
|
||||
"customer_id": 0,
|
||||
"payment_method": "bacs",
|
||||
"payment_method_title": "Bank Transfer",
|
||||
"set_paid": false,
|
||||
"status": "pending",
|
||||
"line_items": [],
|
||||
}),
|
||||
);
|
||||
|
||||
if (response.statusCode == 201) {
|
||||
final data = jsonDecode(response.body);
|
||||
return data['id'];
|
||||
} else {
|
||||
throw Exception('Error creating cart: ${response.body}');
|
||||
}
|
||||
}
|
||||
|
||||
// Get cart items
|
||||
Future<List<CartItem>> getCartItems() async {
|
||||
final url = buildUrl(
|
||||
'https://project2022.amrithaa.com/ecomdemo/wp-json/custom-cart/v1/get?cart_token=my_secret_key_ecom2025',
|
||||
);
|
||||
final response = await http.get(url);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final data = jsonDecode(response.body);
|
||||
final lineItems = data['line_items'] as List;
|
||||
return lineItems.map((item) => CartItem.fromJson(item)).toList();
|
||||
} else {
|
||||
throw Exception('Error getting cart: ${response.body}');
|
||||
}
|
||||
}
|
||||
|
||||
// Add item to cart
|
||||
Future<void> addToCart({
|
||||
required int productId,
|
||||
required int quantity,
|
||||
}) async {
|
||||
final url = Uri.parse(
|
||||
'https://project2022.amrithaa.com/ecomdemo/wp-json/custom-cart/v1/add',
|
||||
);
|
||||
|
||||
final body = {
|
||||
"cart_token": "cart_66fa27d8d44e4",
|
||||
"product_id": productId,
|
||||
"quantity": quantity,
|
||||
};
|
||||
|
||||
final headers = {
|
||||
HttpHeaders.contentTypeHeader: 'application/json',
|
||||
'X-CART-SECRET': 'my_secret_key_ecom2025', // 🔑 replace this
|
||||
};
|
||||
|
||||
print('🔗 URL: $url');
|
||||
print('📋 Headers: $headers');
|
||||
print('📦 Body: $body');
|
||||
|
||||
try {
|
||||
final response = await http.post(
|
||||
url,
|
||||
headers: headers,
|
||||
body: jsonEncode(body),
|
||||
);
|
||||
|
||||
print('📡 Status Code: ${response.statusCode}');
|
||||
print('📡 Response Body: ${response.body}');
|
||||
} catch (e) {
|
||||
print('💥 Exception: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
// Remove cart item
|
||||
|
||||
// Get cart total
|
||||
Future<Map<String, dynamic>> getCartTotal(int cartId) async {
|
||||
final url = buildUrl('/orders/$cartId');
|
||||
final response = await http.get(url);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final data = jsonDecode(response.body);
|
||||
return {
|
||||
'subtotal': data['total'] ?? '0',
|
||||
'tax': data['total_tax'] ?? '0',
|
||||
'shipping': data['shipping_total'] ?? '0',
|
||||
'total': data['total'] ?? '0',
|
||||
};
|
||||
} else {
|
||||
throw Exception('Error getting cart total: ${response.body}');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class AppColors {
|
||||
bool darkTheme = false;
|
||||
|
||||
BaseController baseCtrl = Get.put(BaseController());
|
||||
|
||||
AppColors() {
|
||||
baseCtrl.isDarkModeEnabled.listen((value) {
|
||||
changeColors();
|
||||
});
|
||||
}
|
||||
|
||||
void changeColors() {
|
||||
if (darkTheme == true) {
|
||||
dark = Colors.white;
|
||||
} else {
|
||||
dark = Colors.black;
|
||||
}
|
||||
}
|
||||
|
||||
static Color dark = Colors.black;
|
||||
static const Color background = Color(0xFFffffff);
|
||||
|
||||
}
|
||||
|
||||
class BaseController extends GetxController {
|
||||
RxBool isDarkModeEnabled = false.obs;
|
||||
RxInt currentIndex = 0.obs;
|
||||
String? fbUserId;
|
||||
|
||||
RxMap<String, dynamic> filterData = <String, dynamic>{}.obs;
|
||||
|
||||
void updateFilterData(Map<String, dynamic> newData) {
|
||||
filterData.assignAll(newData);
|
||||
}
|
||||
|
||||
Map<String, dynamic> getFilterData() {
|
||||
return filterData;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
class AppAssets {
|
||||
// SVG icons
|
||||
static const String logo = 'assets/images/wordpresslogo.jpeg';
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:wordpress/providers/api_controller.dart';
|
||||
import 'package:wordpress/viewmodel/cart_screen.dart';
|
||||
|
||||
class CartButtonHelper {
|
||||
static Future<void> addToCartAndNavigate({
|
||||
required BuildContext context,
|
||||
required WidgetRef ref,
|
||||
required product,
|
||||
required int quantity,
|
||||
String? selectedColor,
|
||||
String? selectedWeight,
|
||||
}) async {
|
||||
try {
|
||||
// Show loading indicator
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
SizedBox(width: 12),
|
||||
Text('Adding to cart...'),
|
||||
],
|
||||
),
|
||||
duration: Duration(seconds: 1),
|
||||
),
|
||||
);
|
||||
|
||||
final cartHelper = ref.read(cartHelperProvider);
|
||||
int? cartId = ref.read(cartIdProvider);
|
||||
|
||||
// Create cart if it doesn't exist
|
||||
if (cartId == null) {
|
||||
cartId = await cartHelper.createCart();
|
||||
ref.read(cartIdProvider.notifier).state = cartId;
|
||||
}
|
||||
|
||||
// Add item to cart with attributes
|
||||
await cartHelper.addToCart(
|
||||
productId: product.id ?? 0,
|
||||
quantity: quantity,
|
||||
// color: selectedColor,
|
||||
// size: selectedWeight,
|
||||
);
|
||||
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).hideCurrentSnackBar();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text("Added $quantity x ${product.name} to cart"),
|
||||
backgroundColor: Colors.green[600],
|
||||
action: SnackBarAction(
|
||||
label: "View Cart",
|
||||
textColor: Colors.white,
|
||||
onPressed: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (context) => const CartScreen()),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).hideCurrentSnackBar();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Error adding to cart: $e'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,577 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:wordpress/providers/api_controller.dart';
|
||||
import 'package:wordpress/viewmodel/all_product_model.dart';
|
||||
|
||||
class AllProductScreen extends ConsumerStatefulWidget {
|
||||
const AllProductScreen({super.key});
|
||||
|
||||
@override
|
||||
ConsumerState<AllProductScreen> createState() => _AllProductScreenState();
|
||||
}
|
||||
|
||||
class _AllProductScreenState extends ConsumerState<AllProductScreen>
|
||||
with TickerProviderStateMixin {
|
||||
final ScrollController _scrollController = ScrollController();
|
||||
final int _itemsPerPage = 10;
|
||||
int _currentPage = 1;
|
||||
List<AllProductModel> _displayedProducts = [];
|
||||
bool _isLoadingMore = false;
|
||||
|
||||
late AnimationController _fadeController;
|
||||
late AnimationController _slideController;
|
||||
late Animation<double> _fadeAnimation;
|
||||
late Animation<Offset> _slideAnimation;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_initializeAnimations();
|
||||
_setupScrollListener();
|
||||
}
|
||||
|
||||
void _initializeAnimations() {
|
||||
_fadeController = AnimationController(
|
||||
duration: const Duration(milliseconds: 800),
|
||||
vsync: this,
|
||||
);
|
||||
_slideController = AnimationController(
|
||||
duration: const Duration(milliseconds: 600),
|
||||
vsync: this,
|
||||
);
|
||||
|
||||
_fadeAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
|
||||
CurvedAnimation(parent: _fadeController, curve: Curves.easeInOut),
|
||||
);
|
||||
|
||||
_slideAnimation =
|
||||
Tween<Offset>(begin: const Offset(0, 0.3), end: Offset.zero).animate(
|
||||
CurvedAnimation(parent: _slideController, curve: Curves.easeOutBack),
|
||||
);
|
||||
}
|
||||
|
||||
void _setupScrollListener() {
|
||||
_scrollController.addListener(() {
|
||||
if (_scrollController.position.pixels >=
|
||||
_scrollController.position.maxScrollExtent - 200) {
|
||||
_loadMoreProducts();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _loadMoreProducts() {
|
||||
if (_isLoadingMore) return;
|
||||
|
||||
final asyncProducts = ref.read(allProductProvider);
|
||||
asyncProducts.whenData((allProducts) {
|
||||
if (_displayedProducts.length < allProducts.length) {
|
||||
setState(() {
|
||||
_isLoadingMore = true;
|
||||
});
|
||||
|
||||
Future.delayed(const Duration(milliseconds: 500), () {
|
||||
setState(() {
|
||||
final startIndex = _currentPage * _itemsPerPage;
|
||||
final endIndex = ((startIndex + _itemsPerPage) > allProducts.length)
|
||||
? allProducts.length
|
||||
: startIndex + _itemsPerPage;
|
||||
|
||||
_displayedProducts.addAll(
|
||||
allProducts.sublist(startIndex, endIndex),
|
||||
);
|
||||
_currentPage++;
|
||||
_isLoadingMore = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_scrollController.dispose();
|
||||
_fadeController.dispose();
|
||||
_slideController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final asyncProducts = ref.watch(allProductProvider);
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.grey[50],
|
||||
appBar: AppBar(
|
||||
title: const Text(
|
||||
'All Products',
|
||||
style: TextStyle(fontWeight: FontWeight.bold, color: Colors.white),
|
||||
),
|
||||
backgroundColor: Colors.blue[600],
|
||||
elevation: 0,
|
||||
centerTitle: true,
|
||||
),
|
||||
body: asyncProducts.when(
|
||||
loading: () => _buildLoadingWidget(),
|
||||
error: (err, stack) => _buildErrorWidget(err),
|
||||
data: (products) {
|
||||
if (products.isEmpty) {
|
||||
return _buildEmptyWidget();
|
||||
}
|
||||
|
||||
// Initialize displayed products on first load
|
||||
if (_displayedProducts.isEmpty) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
setState(() {
|
||||
_displayedProducts = products.take(_itemsPerPage).toList();
|
||||
});
|
||||
_fadeController.forward();
|
||||
_slideController.forward();
|
||||
});
|
||||
}
|
||||
|
||||
return _buildProductGrid(products);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLoadingWidget() {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
TweenAnimationBuilder<double>(
|
||||
duration: const Duration(seconds: 1),
|
||||
tween: Tween(begin: 0.0, end: 1.0),
|
||||
builder: (context, value, child) {
|
||||
return Transform.rotate(
|
||||
angle: value * 2 * 3.14159,
|
||||
child: Container(
|
||||
width: 60,
|
||||
height: 60,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(30),
|
||||
gradient: const LinearGradient(
|
||||
colors: [Colors.blue, Colors.purple],
|
||||
),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.shopping_bag,
|
||||
color: Colors.white,
|
||||
size: 30,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Text(
|
||||
"Loading amazing products...",
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Colors.grey,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildErrorWidget(Object error) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.error_outline, size: 64, color: Colors.red[400]),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Oops! Something went wrong',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.grey[800],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Error: $error',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(fontSize: 14, color: Colors.grey[600]),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
ElevatedButton.icon(
|
||||
onPressed: () {
|
||||
ref.invalidate(allProductProvider);
|
||||
},
|
||||
icon: const Icon(Icons.refresh),
|
||||
label: const Text('Retry'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.blue[600],
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEmptyWidget() {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.shopping_bag_outlined, size: 64, color: Colors.grey[400]),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'No products found',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.grey[800],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Check back later for new products',
|
||||
style: TextStyle(fontSize: 14, color: Colors.grey[600]),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildProductGrid(List<AllProductModel> allProducts) {
|
||||
return FadeTransition(
|
||||
opacity: _fadeAnimation,
|
||||
child: SlideTransition(
|
||||
position: _slideAnimation,
|
||||
child: CustomScrollView(
|
||||
controller: _scrollController,
|
||||
physics: const BouncingScrollPhysics(),
|
||||
slivers: [
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
sliver: SliverGrid(
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 2,
|
||||
crossAxisSpacing: 16,
|
||||
mainAxisSpacing: 16,
|
||||
childAspectRatio: 0.75,
|
||||
),
|
||||
delegate: SliverChildBuilderDelegate((context, index) {
|
||||
return AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 500),
|
||||
transitionBuilder: (child, animation) {
|
||||
return SlideTransition(
|
||||
position: Tween<Offset>(
|
||||
begin: const Offset(0, 0.3),
|
||||
end: Offset.zero,
|
||||
).animate(animation),
|
||||
child: FadeTransition(opacity: animation, child: child),
|
||||
);
|
||||
},
|
||||
child: ProductCard(
|
||||
product: _displayedProducts[index],
|
||||
key: ValueKey(_displayedProducts[index].id),
|
||||
animationDelay: Duration(milliseconds: index * 100),
|
||||
),
|
||||
);
|
||||
}, childCount: _displayedProducts.length),
|
||||
),
|
||||
),
|
||||
if (_isLoadingMore ||
|
||||
_displayedProducts.length < allProducts.length)
|
||||
SliverToBoxAdapter(child: _buildLoadMoreWidget()),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLoadMoreWidget() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Center(
|
||||
child: _isLoadingMore
|
||||
? Column(
|
||||
children: [
|
||||
CircularProgressIndicator(
|
||||
valueColor: AlwaysStoppedAnimation<Color>(
|
||||
Colors.blue[600]!,
|
||||
),
|
||||
strokeWidth: 2,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
'Loading more products...',
|
||||
style: TextStyle(color: Colors.grey[600], fontSize: 14),
|
||||
),
|
||||
],
|
||||
)
|
||||
: ElevatedButton.icon(
|
||||
onPressed: _loadMoreProducts,
|
||||
icon: const Icon(Icons.expand_more),
|
||||
label: const Text('Load More'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.blue[600],
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 24,
|
||||
vertical: 12,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(25),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ProductCard extends StatefulWidget {
|
||||
final AllProductModel product;
|
||||
final Duration animationDelay;
|
||||
|
||||
const ProductCard({
|
||||
super.key,
|
||||
required this.product,
|
||||
this.animationDelay = Duration.zero,
|
||||
});
|
||||
|
||||
@override
|
||||
State<ProductCard> createState() => _ProductCardState();
|
||||
}
|
||||
|
||||
class _ProductCardState extends State<ProductCard>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late AnimationController _hoverController;
|
||||
late Animation<double> _scaleAnimation;
|
||||
late Animation<double> _elevationAnimation;
|
||||
bool _isVisible = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_hoverController = AnimationController(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
vsync: this,
|
||||
);
|
||||
|
||||
_scaleAnimation = Tween<double>(begin: 1.0, end: 1.05).animate(
|
||||
CurvedAnimation(parent: _hoverController, curve: Curves.easeInOut),
|
||||
);
|
||||
|
||||
_elevationAnimation = Tween<double>(begin: 4.0, end: 12.0).animate(
|
||||
CurvedAnimation(parent: _hoverController, curve: Curves.easeInOut),
|
||||
);
|
||||
|
||||
// Staggered animation entrance
|
||||
Future.delayed(widget.animationDelay, () {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isVisible = true;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_hoverController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedOpacity(
|
||||
opacity: _isVisible ? 1.0 : 0.0,
|
||||
duration: const Duration(milliseconds: 600),
|
||||
child: AnimatedBuilder(
|
||||
animation: _hoverController,
|
||||
builder: (context, child) {
|
||||
return Transform.scale(
|
||||
scale: _scaleAnimation.value,
|
||||
child: GestureDetector(
|
||||
onTapDown: (_) => _hoverController.forward(),
|
||||
onTapUp: (_) => _hoverController.reverse(),
|
||||
onTapCancel: () => _hoverController.reverse(),
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
blurRadius: _elevationAnimation.value,
|
||||
offset: Offset(0, _elevationAnimation.value / 2),
|
||||
spreadRadius: 1,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [_buildProductImage(), _buildProductDetails()],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildProductImage() {
|
||||
return Expanded(
|
||||
flex: 3,
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
decoration: const BoxDecoration(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
||||
),
|
||||
child: Stack(
|
||||
children: [
|
||||
Hero(
|
||||
tag: 'product_${widget.product.id}',
|
||||
child: Image.network(
|
||||
widget.product.images.isNotEmpty
|
||||
? widget.product.images.first.src
|
||||
: '',
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
fit: BoxFit.cover,
|
||||
loadingBuilder: (context, child, loadingProgress) {
|
||||
if (loadingProgress == null) return child;
|
||||
return Container(
|
||||
color: Colors.grey[100],
|
||||
child: Center(
|
||||
child: CircularProgressIndicator(
|
||||
value: loadingProgress.expectedTotalBytes != null
|
||||
? loadingProgress.cumulativeBytesLoaded /
|
||||
loadingProgress.expectedTotalBytes!
|
||||
: null,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(
|
||||
Colors.blue[300]!,
|
||||
),
|
||||
strokeWidth: 2,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
errorBuilder: (context, error, stackTrace) => Container(
|
||||
color: Colors.grey[100],
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.image_not_supported_outlined,
|
||||
color: Colors.grey[400],
|
||||
size: 32,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Image not available',
|
||||
style: TextStyle(color: Colors.grey[500], fontSize: 12),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
// Gradient overlay
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Container(
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
Colors.transparent,
|
||||
Colors.black.withOpacity(0.15),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildProductDetails() {
|
||||
return Expanded(
|
||||
flex: 3,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
widget.product.name,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.black87,
|
||||
height: 1.3,
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
"₹${widget.product.price}",
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.green[600],
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.blue[600],
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.blue.withOpacity(0.3),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.add_shopping_cart,
|
||||
color: Colors.white,
|
||||
size: 18,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,724 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:wordpress/providers/api_controller.dart';
|
||||
import 'package:wordpress/viewmodel/cart_model.dart';
|
||||
|
||||
// ✅ Checkout Screen with similar design to Cart Screen
|
||||
class CheckOutScreen extends ConsumerWidget {
|
||||
const CheckOutScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final cartAsync = ref.watch(cartProvider);
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.grey.shade50,
|
||||
appBar: AppBar(
|
||||
title: const Text(
|
||||
"Checkout",
|
||||
style: TextStyle(
|
||||
color: Colors.black87,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 20,
|
||||
),
|
||||
),
|
||||
backgroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
shadowColor: Colors.black12,
|
||||
iconTheme: const IconThemeData(color: Colors.black87),
|
||||
systemOverlayStyle: SystemUiOverlayStyle.dark,
|
||||
actions: [
|
||||
cartAsync.when(
|
||||
data: (cartResponse) => cartResponse.cart.isNotEmpty
|
||||
? Container(
|
||||
margin: const EdgeInsets.only(right: 16),
|
||||
child: Center(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 6,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.orange.shade100,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Text(
|
||||
"${cartResponse.cart.length} items",
|
||||
style: TextStyle(
|
||||
color: Colors.orange.shade800,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
: const SizedBox(),
|
||||
loading: () => const SizedBox(),
|
||||
error: (_, __) => const SizedBox(),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: cartAsync.when(
|
||||
data: (cartResponse) {
|
||||
if (cartResponse.cart.isEmpty) {
|
||||
return _buildEmptyCart(context);
|
||||
}
|
||||
|
||||
final items = cartResponse.cart.values.toList();
|
||||
|
||||
return SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
// Header with item count
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 20,
|
||||
vertical: 16,
|
||||
),
|
||||
color: Colors.white,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
"Review your order",
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.grey.shade800,
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 6,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.orange.shade100,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Text(
|
||||
"${items.length} items",
|
||||
style: TextStyle(
|
||||
color: Colors.orange.shade800,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 8),
|
||||
|
||||
// Cart Items
|
||||
ListView.separated(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemCount: items.length,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 8,
|
||||
),
|
||||
separatorBuilder: (context, index) =>
|
||||
const SizedBox(height: 12),
|
||||
itemBuilder: (context, index) {
|
||||
final item = items[index];
|
||||
return _buildCheckoutItem(context, item);
|
||||
},
|
||||
),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Delivery Address Section
|
||||
_buildAddressSection(context),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Payment Method Section
|
||||
_buildPaymentSection(context),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Cart Summary
|
||||
_buildCheckoutSummary(context, items),
|
||||
|
||||
// Add extra space for bottom navigation
|
||||
const SizedBox(height: 100),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
loading: () => Container(
|
||||
color: Colors.white,
|
||||
child: const Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
CircularProgressIndicator(
|
||||
valueColor: AlwaysStoppedAnimation<Color>(Colors.orange),
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
Text(
|
||||
"Loading your order...",
|
||||
style: TextStyle(fontSize: 16, color: Colors.grey),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
error: (err, stack) => Container(
|
||||
color: Colors.white,
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.error_outline, size: 64, color: Colors.red.shade300),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
"Something went wrong",
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.grey.shade800,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
"Please try again later",
|
||||
style: TextStyle(fontSize: 14, color: Colors.grey.shade600),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
ElevatedButton(
|
||||
onPressed: () => ref.invalidate(cartProvider),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.orange,
|
||||
foregroundColor: Colors.white,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
child: const Text("Retry"),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
// Bottom Checkout Button
|
||||
bottomNavigationBar: cartAsync.when(
|
||||
data: (cartResponse) {
|
||||
if (cartResponse.cart.isEmpty) return const SizedBox();
|
||||
|
||||
final items = cartResponse.cart.values.toList();
|
||||
final total = _calculateTotal(items);
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, -4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: SafeArea(
|
||||
child: SizedBox(
|
||||
height: 56,
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text("Processing payment..."),
|
||||
backgroundColor: Colors.green,
|
||||
),
|
||||
);
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.orange,
|
||||
foregroundColor: Colors.white,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
elevation: 3,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Icons.payment),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
"Pay ₹${total.toStringAsFixed(2)}",
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
loading: () => const SizedBox(),
|
||||
error: (_, __) => const SizedBox(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEmptyCart(BuildContext context) {
|
||||
return Container(
|
||||
color: Colors.white,
|
||||
width: double.infinity,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(24),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.orange.shade50,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(
|
||||
Icons.shopping_cart_outlined,
|
||||
size: 80,
|
||||
color: Colors.orange.shade300,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
"Your cart is empty",
|
||||
style: TextStyle(
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.grey.shade800,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
"Add items to checkout",
|
||||
style: TextStyle(fontSize: 16, color: Colors.grey.shade600),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
ElevatedButton.icon(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
icon: const Icon(Icons.shopping_bag_outlined),
|
||||
label: const Text("Continue Shopping"),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.orange,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(25),
|
||||
),
|
||||
elevation: 2,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCheckoutItem(BuildContext context, CartItem item) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade50,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: Colors.grey.shade200),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Row(
|
||||
children: [
|
||||
// Product Image/Icon
|
||||
Container(
|
||||
width: 70,
|
||||
height: 70,
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [Colors.orange.shade100, Colors.orange.shade50],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Icon(
|
||||
Icons.shopping_bag_outlined,
|
||||
size: 32,
|
||||
color: Colors.orange.shade600,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
|
||||
// Product Info
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Product Name
|
||||
Text(
|
||||
item.name,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.black87,
|
||||
),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
// Price and Quantity
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
// Price
|
||||
Text(
|
||||
"₹${((double.tryParse(item.price) ?? 0) * item.quantity).toStringAsFixed(2)}",
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: Colors.orange.shade700,
|
||||
),
|
||||
),
|
||||
|
||||
// Quantity
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 4,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: Colors.grey.shade300),
|
||||
),
|
||||
child: Text(
|
||||
"Qty: ${item.quantity}",
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.grey.shade700,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAddressSection(BuildContext context) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16),
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: Colors.grey.shade200),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
"Delivery Address",
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.grey.shade800,
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {},
|
||||
child: Text(
|
||||
"Change",
|
||||
style: TextStyle(
|
||||
color: Colors.orange.shade700,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.location_on_outlined,
|
||||
color: Colors.orange.shade600,
|
||||
size: 20,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"Home",
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.grey.shade800,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
"123 Main Street, Apartment 4B, City, State - 123456",
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
"Phone: +91 9876543210",
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPaymentSection(BuildContext context) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16),
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: Colors.grey.shade200),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
"Payment Method",
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.grey.shade800,
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {},
|
||||
child: Text(
|
||||
"Change",
|
||||
style: TextStyle(
|
||||
color: Colors.orange.shade700,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.blue.shade50,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Icon(
|
||||
Icons.credit_card,
|
||||
color: Colors.blue.shade700,
|
||||
size: 24,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
"Credit/Debit Card",
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.grey.shade800,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
Icon(Icons.check_circle, color: Colors.green.shade600),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCheckoutSummary(BuildContext context, List<CartItem> items) {
|
||||
double subtotal = 0;
|
||||
int totalItems = 0;
|
||||
|
||||
for (var item in items) {
|
||||
subtotal += (double.tryParse(item.price) ?? 0) * item.quantity;
|
||||
totalItems += item.quantity;
|
||||
}
|
||||
|
||||
final shipping = subtotal > 500 ? 0.0 : 50.0;
|
||||
final tax = subtotal * 0.18; // 18% GST
|
||||
final total = subtotal + shipping + tax;
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16),
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: Colors.grey.shade200),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Summary Header
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Text(
|
||||
"Order Summary",
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
"$totalItems items",
|
||||
style: TextStyle(fontSize: 14, color: Colors.grey.shade600),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Price Breakdown
|
||||
_buildPriceRow("Subtotal", subtotal),
|
||||
const SizedBox(height: 8),
|
||||
_buildPriceRow(
|
||||
"Shipping",
|
||||
shipping,
|
||||
subtitle: shipping == 0 ? "Free shipping!" : null,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
_buildPriceRow("GST (18%)", tax),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
const Divider(thickness: 1),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Total
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Text(
|
||||
"Total",
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
"₹${total.toStringAsFixed(2)}",
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: Colors.orange.shade700,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPriceRow(String label, double amount, {String? subtitle}) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(fontSize: 14, color: Colors.grey.shade700),
|
||||
),
|
||||
if (subtitle != null)
|
||||
Text(
|
||||
subtitle,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.green.shade600,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Text(
|
||||
amount == 0 ? "FREE" : "₹${amount.toStringAsFixed(2)}",
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: amount == 0 ? Colors.green.shade600 : Colors.black87,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
double _calculateTotal(List<CartItem> items) {
|
||||
double subtotal = 0;
|
||||
for (var item in items) {
|
||||
subtotal += (double.tryParse(item.price) ?? 0) * item.quantity;
|
||||
}
|
||||
final shipping = subtotal > 500 ? 0.0 : 50.0;
|
||||
final tax = subtotal * 0.18;
|
||||
return subtotal + shipping + tax;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,802 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_widget_from_html/flutter_widget_from_html.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:wordpress/providers/api_controller.dart';
|
||||
import 'package:wordpress/utils/ftaf.dart';
|
||||
import 'package:wordpress/viewmodel/cart_screen.dart';
|
||||
|
||||
// Providers
|
||||
final selectedColorProvider = StateProvider<String?>((ref) => null);
|
||||
final selectedWeightProvider = StateProvider<String?>((ref) => null);
|
||||
final quantityProvider = StateProvider<int>((ref) => 1);
|
||||
final currentImageIndexProvider = StateProvider<int>((ref) => 0);
|
||||
final isFavoriteProvider = StateProvider<bool>((ref) => false);
|
||||
|
||||
class BigBasketDetailsScreen extends ConsumerWidget {
|
||||
final int productId;
|
||||
|
||||
const BigBasketDetailsScreen({super.key, required this.productId});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final detailAsync = ref.watch(productDetailProvider(productId));
|
||||
final selectedColor = ref.watch(selectedColorProvider);
|
||||
final selectedWeight = ref.watch(selectedWeightProvider);
|
||||
final quantity = ref.watch(quantityProvider);
|
||||
final currentImageIndex = ref.watch(currentImageIndexProvider);
|
||||
final isFavorite = ref.watch(isFavoriteProvider);
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.grey[50],
|
||||
body: detailAsync.when(
|
||||
data: (product) {
|
||||
final images = product.images ?? [];
|
||||
String? mainImage = images.isNotEmpty ? images.first.src : null;
|
||||
|
||||
// Update main image based on selected color
|
||||
if (selectedColor != null && images.isNotEmpty) {
|
||||
final match = images.firstWhere(
|
||||
(img) =>
|
||||
(img.name != null &&
|
||||
img.name!.toLowerCase() == selectedColor.toLowerCase()) ||
|
||||
(img.alt != null &&
|
||||
img.alt!.toLowerCase() == selectedColor.toLowerCase()),
|
||||
orElse: () => images.first,
|
||||
);
|
||||
mainImage = match.src ?? mainImage;
|
||||
}
|
||||
|
||||
return CustomScrollView(
|
||||
slivers: [
|
||||
// Custom App Bar with image
|
||||
SliverAppBar(
|
||||
expandedHeight: 350,
|
||||
pinned: true,
|
||||
backgroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
leading: Container(
|
||||
margin: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.9),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: IconButton(
|
||||
icon: const Icon(Icons.arrow_back, color: Colors.black),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
Container(
|
||||
margin: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.9),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: IconButton(
|
||||
icon: Icon(
|
||||
isFavorite ? Icons.favorite : Icons.favorite_border,
|
||||
color: isFavorite ? Colors.red : Colors.black,
|
||||
),
|
||||
onPressed: () {
|
||||
ref.read(isFavoriteProvider.notifier).state =
|
||||
!isFavorite;
|
||||
},
|
||||
),
|
||||
),
|
||||
Container(
|
||||
margin: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.9),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: IconButton(
|
||||
icon: const Icon(Icons.share, color: Colors.black),
|
||||
onPressed: () {
|
||||
// Share functionality
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
flexibleSpace: FlexibleSpaceBar(
|
||||
background: Container(
|
||||
color: Colors.white,
|
||||
child: images.isNotEmpty
|
||||
? PageView.builder(
|
||||
itemCount: images.length,
|
||||
onPageChanged: (index) {
|
||||
ref
|
||||
.read(currentImageIndexProvider.notifier)
|
||||
.state =
|
||||
index;
|
||||
},
|
||||
itemBuilder: (context, index) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
color: Colors.grey[100],
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Image.network(
|
||||
images[index].src ?? '',
|
||||
fit: BoxFit.contain,
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
return Container(
|
||||
color: Colors.grey[200],
|
||||
child: const Center(
|
||||
child: Icon(
|
||||
Icons.image_not_supported,
|
||||
size: 80,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
)
|
||||
: Container(
|
||||
margin: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
color: Colors.grey[200],
|
||||
),
|
||||
child: const Center(
|
||||
child: Icon(
|
||||
Icons.image_not_supported,
|
||||
size: 100,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Product Content
|
||||
SliverToBoxAdapter(
|
||||
child: Container(
|
||||
color: Colors.white,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Image indicators
|
||||
if (images.length > 1)
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: List.generate(
|
||||
images.length,
|
||||
(index) => Container(
|
||||
margin: const EdgeInsets.symmetric(
|
||||
horizontal: 4,
|
||||
),
|
||||
width: 8,
|
||||
height: 8,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: currentImageIndex == index
|
||||
? Colors.green[600]
|
||||
: Colors.grey[300],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Product Title & Brand
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
product.name ?? "Product Name",
|
||||
style: const TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'Price : ₹ ${product.price.toString()}',
|
||||
style: const TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 4,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.green[50],
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
border: Border.all(
|
||||
color: Colors.green[200]!,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.star,
|
||||
size: 14,
|
||||
color: Colors.green[600],
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
"4.2",
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.green[600],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
"1,234 reviews",
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Attributes Section
|
||||
if (product.attributes != null &&
|
||||
product.attributes!.isNotEmpty)
|
||||
_buildAttributesSection(
|
||||
product,
|
||||
ref,
|
||||
selectedColor,
|
||||
selectedWeight,
|
||||
),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Quantity Selector
|
||||
Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16),
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: Colors.grey[300]!),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Text(
|
||||
"Quantity:",
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: Colors.grey[300]!),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
InkWell(
|
||||
onTap: () {
|
||||
if (quantity > 1) {
|
||||
ref
|
||||
.read(quantityProvider.notifier)
|
||||
.state--;
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: const Icon(
|
||||
Icons.remove,
|
||||
size: 18,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 12,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.symmetric(
|
||||
vertical: BorderSide(
|
||||
color: Colors.grey[300]!,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
quantity.toString(),
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
InkWell(
|
||||
onTap: () {
|
||||
ref
|
||||
.read(quantityProvider.notifier)
|
||||
.state++;
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Icon(
|
||||
Icons.add,
|
||||
size: 18,
|
||||
color: Colors.green[600],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Product Description
|
||||
if (product.shortDescription != null &&
|
||||
product.shortDescription!.isNotEmpty)
|
||||
Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16),
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: Colors.grey[300]!),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
"Product Details",
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
HtmlWidget(
|
||||
product.shortDescription!,
|
||||
textStyle: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey[700],
|
||||
height: 1.5,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Delivery Info
|
||||
Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16),
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.blue[50],
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: Colors.blue[100]!),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.local_shipping,
|
||||
color: Colors.blue[700],
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
"Free Delivery",
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
"Get it by tomorrow, 6:00 AM - 10:00 AM",
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.assignment_return,
|
||||
color: Colors.blue[700],
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
"Easy Return",
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
"7 days return policy",
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 100), // Space for bottom buttons
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
loading: () =>
|
||||
const Scaffold(body: Center(child: CircularProgressIndicator())),
|
||||
error: (err, stack) =>
|
||||
Scaffold(body: Center(child: Text("Error: $err"))),
|
||||
),
|
||||
bottomNavigationBar: detailAsync.when(
|
||||
data: (product) => _buildBottomBar(
|
||||
context,
|
||||
ref,
|
||||
product,
|
||||
selectedColor,
|
||||
selectedWeight,
|
||||
quantity,
|
||||
),
|
||||
loading: () => const SizedBox.shrink(),
|
||||
error: (err, stack) => const SizedBox.shrink(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAttributesSection(
|
||||
product,
|
||||
WidgetRef ref,
|
||||
String? selectedColor,
|
||||
String? selectedWeight,
|
||||
) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16),
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: Colors.grey[300]!),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: product.attributes!.map<Widget>((attr) {
|
||||
final attrName = attr.name?.toLowerCase();
|
||||
|
||||
if (attrName == "color") {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
"Available Colors",
|
||||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Wrap(
|
||||
spacing: 12,
|
||||
runSpacing: 8,
|
||||
children: attr.options!.map<Widget>((option) {
|
||||
final isSelected = option == selectedColor;
|
||||
Color colorValue = _getColorFromName(option);
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
ref.read(selectedColorProvider.notifier).state = option;
|
||||
},
|
||||
child: Container(
|
||||
width: 45,
|
||||
height: 45,
|
||||
decoration: BoxDecoration(
|
||||
color: colorValue,
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(
|
||||
color: isSelected
|
||||
? Colors.green[600]!
|
||||
: Colors.grey[400]!,
|
||||
width: isSelected ? 3 : 1,
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: isSelected
|
||||
? Icon(
|
||||
Icons.check,
|
||||
color: colorValue == Colors.white
|
||||
? Colors.black
|
||||
: Colors.white,
|
||||
size: 20,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
);
|
||||
} else if (attrName == "weight" || attrName == "size") {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"Select ${attr.name}",
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Wrap(
|
||||
spacing: 12,
|
||||
runSpacing: 8,
|
||||
children: attr.options!.map<Widget>((option) {
|
||||
final isSelected = option == selectedWeight;
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
ref.read(selectedWeightProvider.notifier).state =
|
||||
option;
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 12,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected ? Colors.green[600] : Colors.white,
|
||||
border: Border.all(
|
||||
color: isSelected
|
||||
? Colors.green[600]!
|
||||
: Colors.grey[400]!,
|
||||
width: 1.5,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(25),
|
||||
),
|
||||
child: Text(
|
||||
option,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: isSelected ? Colors.white : Colors.black,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return const SizedBox.shrink();
|
||||
}).toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBottomBar(
|
||||
BuildContext context,
|
||||
WidgetRef ref,
|
||||
product,
|
||||
String? selectedColor,
|
||||
String? selectedWeight,
|
||||
int quantity,
|
||||
) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, -5),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: SafeArea(
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.green[600],
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
elevation: 2,
|
||||
),
|
||||
onPressed: () {
|
||||
if (_validateSelection(
|
||||
context,
|
||||
selectedColor,
|
||||
selectedWeight,
|
||||
product,
|
||||
)) {
|
||||
// Handle Add to Cart
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
"Added $quantity x ${product.name} to cart",
|
||||
),
|
||||
backgroundColor: Colors.green[600],
|
||||
action: SnackBarAction(
|
||||
label: "View Cart",
|
||||
textColor: Colors.white,
|
||||
onPressed: () async {
|
||||
await CartButtonHelper.addToCartAndNavigate(
|
||||
context: context,
|
||||
ref: ref,
|
||||
product: product,
|
||||
quantity: quantity,
|
||||
selectedColor: selectedColor,
|
||||
selectedWeight: selectedWeight,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
child: const Text(
|
||||
"Add to Cart",
|
||||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
bool _validateSelection(
|
||||
BuildContext context,
|
||||
String? selectedColor,
|
||||
String? selectedWeight,
|
||||
product,
|
||||
) {
|
||||
bool hasColorAttribute =
|
||||
product.attributes?.any(
|
||||
(attr) => attr.name?.toLowerCase() == "color",
|
||||
) ??
|
||||
false;
|
||||
bool hasWeightAttribute =
|
||||
product.attributes?.any(
|
||||
(attr) =>
|
||||
attr.name?.toLowerCase() == "weight" ||
|
||||
attr.name?.toLowerCase() == "size",
|
||||
) ??
|
||||
false;
|
||||
|
||||
if (hasColorAttribute && selectedColor == null) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text("Please select a color"),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (hasWeightAttribute && selectedWeight == null) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text("Please select size/weight"),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
Color _getColorFromName(String colorName) {
|
||||
if (colorName.startsWith("#") && colorName.length == 7) {
|
||||
return Color(
|
||||
int.parse(colorName.substring(1, 7), radix: 16) + 0xFF000000,
|
||||
);
|
||||
}
|
||||
|
||||
switch (colorName.toLowerCase()) {
|
||||
case "red":
|
||||
return Colors.red;
|
||||
case "blue":
|
||||
return Colors.blue;
|
||||
case "green":
|
||||
return Colors.green;
|
||||
case "black":
|
||||
return Colors.black;
|
||||
case "white":
|
||||
return Colors.white;
|
||||
case "orange":
|
||||
return Colors.orange;
|
||||
case "yellow":
|
||||
return Colors.yellow;
|
||||
case "purple":
|
||||
return Colors.purple;
|
||||
case "pink":
|
||||
return Colors.pink;
|
||||
case "brown":
|
||||
return Colors.brown;
|
||||
default:
|
||||
return Colors.grey;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,687 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:carousel_slider/carousel_slider.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:wordpress/providers/api_const.dart';
|
||||
import 'package:wordpress/providers/api_controller.dart';
|
||||
import 'package:wordpress/utils/app_assests.dart';
|
||||
import 'package:wordpress/view/all_product_screen.dart';
|
||||
import 'package:wordpress/view/details_screen.dart';
|
||||
import 'package:wordpress/viewmodel/cart_screen.dart';
|
||||
import 'package:wordpress/viewmodel/product_home_page_model.dart';
|
||||
|
||||
class HomeScreen extends ConsumerStatefulWidget {
|
||||
const HomeScreen({super.key});
|
||||
|
||||
@override
|
||||
ConsumerState<HomeScreen> createState() => _HomeScreenState();
|
||||
}
|
||||
|
||||
class _HomeScreenState extends ConsumerState<HomeScreen>
|
||||
with TickerProviderStateMixin {
|
||||
late AnimationController _fadeController;
|
||||
late AnimationController _slideController;
|
||||
late AnimationController _scaleController;
|
||||
|
||||
late Animation<double> _fadeAnimation;
|
||||
late Animation<Offset> _slideAnimation;
|
||||
late Animation<double> _scaleAnimation;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
// Initialize animation controllers
|
||||
_fadeController = AnimationController(
|
||||
duration: const Duration(milliseconds: 800),
|
||||
vsync: this,
|
||||
);
|
||||
|
||||
_slideController = AnimationController(
|
||||
duration: const Duration(milliseconds: 1000),
|
||||
vsync: this,
|
||||
);
|
||||
|
||||
_scaleController = AnimationController(
|
||||
duration: const Duration(milliseconds: 600),
|
||||
vsync: this,
|
||||
);
|
||||
|
||||
// Initialize animations
|
||||
_fadeAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
|
||||
CurvedAnimation(parent: _fadeController, curve: Curves.easeInOut),
|
||||
);
|
||||
|
||||
_slideAnimation =
|
||||
Tween<Offset>(begin: const Offset(0, 0.3), end: Offset.zero).animate(
|
||||
CurvedAnimation(parent: _slideController, curve: Curves.easeOutBack),
|
||||
);
|
||||
|
||||
_scaleAnimation = Tween<double>(begin: 0.8, end: 1.0).animate(
|
||||
CurvedAnimation(parent: _scaleController, curve: Curves.elasticOut),
|
||||
);
|
||||
|
||||
// Start animations
|
||||
_startAnimations();
|
||||
}
|
||||
|
||||
void _startAnimations() async {
|
||||
await Future.delayed(const Duration(milliseconds: 100));
|
||||
_fadeController.forward();
|
||||
|
||||
await Future.delayed(const Duration(milliseconds: 200));
|
||||
_slideController.forward();
|
||||
|
||||
await Future.delayed(const Duration(milliseconds: 300));
|
||||
_scaleController.forward();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_fadeController.dispose();
|
||||
_slideController.dispose();
|
||||
_scaleController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
String getFullImageUrl(String url) {
|
||||
if (url.startsWith("http")) {
|
||||
return url;
|
||||
}
|
||||
return "${ConstsApi.baseUrl}$url";
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final homeAsync = ref.watch(productHomePageProvider);
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.grey[50],
|
||||
body: SafeArea(
|
||||
child: homeAsync.when(
|
||||
data: (data) {
|
||||
return SingleChildScrollView(
|
||||
physics: const BouncingScrollPhysics(),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
ScaleTransition(
|
||||
scale: _scaleAnimation,
|
||||
child: _buildHeaderSection(),
|
||||
),
|
||||
// 🎯 Section 1: Animated Banner Carousel
|
||||
FadeTransition(
|
||||
opacity: _fadeAnimation,
|
||||
child: _buildBannerSection(data.banners),
|
||||
),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// 🎯 Section 2: Animated Categories
|
||||
SlideTransition(
|
||||
position: _slideAnimation,
|
||||
child: _buildCategoriesSection(data.categories),
|
||||
),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// 🎯 Section 3: Animated Latest Products
|
||||
ScaleTransition(
|
||||
scale: _scaleAnimation,
|
||||
child: _buildLatestProductsSection(data.latest),
|
||||
),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
loading: () => _buildLoadingState(),
|
||||
error: (err, stack) => _buildErrorState(err),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 🎨 Banner Section with Animation
|
||||
Widget _buildBannerSection(List<BannerModel> banners) {
|
||||
if (banners.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return Container(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(height: 25),
|
||||
CarouselSlider(
|
||||
options: CarouselOptions(
|
||||
height: 160,
|
||||
autoPlay: true,
|
||||
enlargeCenterPage: true,
|
||||
viewportFraction: 0.9,
|
||||
autoPlayInterval: const Duration(seconds: 4),
|
||||
autoPlayAnimationDuration: const Duration(milliseconds: 800),
|
||||
autoPlayCurve: Curves.fastOutSlowIn,
|
||||
),
|
||||
items: banners.map((banner) {
|
||||
final url = getFullImageUrl(banner.image);
|
||||
return ClipRRect(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child: Container(
|
||||
color: Colors.grey[200], // fallback bg
|
||||
child: Image.network(
|
||||
url,
|
||||
width: double.infinity,
|
||||
height: 160,
|
||||
fit: BoxFit
|
||||
.fill, // fills both width & height // change to BoxFit.contain if you don’t want cropping
|
||||
errorBuilder: (context, error, stackTrace) => Image.network(
|
||||
'https://project2022.amrithaa.com/ecomdemo/wp-content/uploads/2025/08/product1.jpg',
|
||||
width: double.infinity,
|
||||
height: 160,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeaderSection() {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Image.asset(AppAssets.logo, height: 60, width: 100),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
Get.to(CartScreen());
|
||||
},
|
||||
child: Container(
|
||||
width: 40,
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
|
||||
shape: BoxShape.circle,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
blurRadius: 6,
|
||||
offset: const Offset(0, 3),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Icon(Icons.shopping_cart, color: Colors.blue),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 🎨 Categories Section with Animation
|
||||
Widget _buildCategoriesSection(List<CategoryModel> categories) {
|
||||
if (categories.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
"Shop by Category",
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
SizedBox(
|
||||
height: 130,
|
||||
child: ListView.builder(
|
||||
scrollDirection: Axis.horizontal,
|
||||
physics: const BouncingScrollPhysics(),
|
||||
itemCount: categories.length,
|
||||
itemBuilder: (context, index) {
|
||||
final category = categories[index];
|
||||
return TweenAnimationBuilder<double>(
|
||||
duration: Duration(milliseconds: 400 + (index * 100)),
|
||||
tween: Tween(begin: 0.0, end: 1.0),
|
||||
builder: (context, value, child) {
|
||||
return Transform.scale(
|
||||
scale: value,
|
||||
child: Container(
|
||||
width: 100,
|
||||
margin: const EdgeInsets.only(right: 12),
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
height: 80,
|
||||
width: 80,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors
|
||||
.primaries[index % Colors.primaries.length]
|
||||
.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(
|
||||
color: Colors
|
||||
.primaries[index %
|
||||
Colors.primaries.length]
|
||||
.withOpacity(0.3),
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
child: category.image != null
|
||||
? ClipRRect(
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
child: Image.network(
|
||||
getFullImageUrl(category.image!),
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder:
|
||||
(
|
||||
context,
|
||||
error,
|
||||
stackTrace,
|
||||
) => Icon(
|
||||
Icons.category,
|
||||
color:
|
||||
Colors.primaries[index %
|
||||
Colors.primaries.length],
|
||||
size: 30,
|
||||
),
|
||||
),
|
||||
)
|
||||
: Icon(
|
||||
Icons.category,
|
||||
color:
|
||||
Colors.primaries[index %
|
||||
Colors.primaries.length],
|
||||
size: 30,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
category.name,
|
||||
textAlign: TextAlign.center,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 🎨 Latest Products Section with Animation
|
||||
Widget _buildLatestProductsSection(List<ProductModel> products) {
|
||||
if (products.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Text(
|
||||
"Latest Products",
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (context) => AllProductScreen()),
|
||||
);
|
||||
},
|
||||
child: const Text(
|
||||
"View All",
|
||||
style: TextStyle(
|
||||
color: Colors.blue,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
GridView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 2,
|
||||
childAspectRatio: 0.75,
|
||||
crossAxisSpacing: 12,
|
||||
mainAxisSpacing: 12,
|
||||
),
|
||||
itemCount: products.length > 6 ? 6 : products.length,
|
||||
itemBuilder: (context, index) {
|
||||
final product = products[index];
|
||||
return TweenAnimationBuilder<double>(
|
||||
duration: Duration(milliseconds: 500 + (index * 150)),
|
||||
tween: Tween(begin: 0.0, end: 1.0),
|
||||
builder: (context, value, child) {
|
||||
return Transform.translate(
|
||||
offset: Offset(0, 30 * (1 - value)),
|
||||
child: Opacity(
|
||||
opacity: value,
|
||||
child: _buildProductCard(product),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 🎨 Product Card with Hover Effect
|
||||
Widget _buildProductCard(ProductModel product) {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => BigBasketDetailsScreen(productId: product.id),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.08),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Product Image
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: Container(
|
||||
decoration: const BoxDecoration(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: const BorderRadius.vertical(
|
||||
top: Radius.circular(16),
|
||||
),
|
||||
child: Stack(
|
||||
children: [
|
||||
Image.network(
|
||||
getFullImageUrl(product.image),
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (context, error, stackTrace) => Container(
|
||||
color: Colors.grey[100],
|
||||
child: const Icon(
|
||||
Icons.image_not_supported,
|
||||
color: Colors.grey,
|
||||
size: 40,
|
||||
),
|
||||
),
|
||||
),
|
||||
// Gradient overlay
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Container(
|
||||
height: 30,
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
Colors.transparent,
|
||||
Colors.black.withOpacity(0.1),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Product Details
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
product.name,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.black87,
|
||||
height: 1.2,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
"₹${product.price}",
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.green,
|
||||
),
|
||||
),
|
||||
GestureDetector(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(6),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.blue,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.add_shopping_cart,
|
||||
color: Colors.white,
|
||||
size: 16,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 🎨 Loading State with Animation
|
||||
Widget _buildLoadingState() {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
TweenAnimationBuilder<double>(
|
||||
duration: const Duration(seconds: 1),
|
||||
tween: Tween(begin: 0.0, end: 1.0),
|
||||
builder: (context, value, child) {
|
||||
return Transform.rotate(
|
||||
angle: value * 2 * 3.14159,
|
||||
child: Container(
|
||||
width: 60,
|
||||
height: 60,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(30),
|
||||
gradient: const LinearGradient(
|
||||
colors: [Colors.blue, Colors.purple],
|
||||
),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.shopping_bag,
|
||||
color: Colors.white,
|
||||
size: 30,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Text(
|
||||
"Loading amazing products...",
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Colors.grey,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 🎨 Error State
|
||||
Widget _buildErrorState(dynamic error) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
TweenAnimationBuilder<double>(
|
||||
duration: const Duration(milliseconds: 800),
|
||||
tween: Tween(begin: 0.0, end: 1.0),
|
||||
builder: (context, value, child) {
|
||||
return Transform.scale(
|
||||
scale: value,
|
||||
child: Container(
|
||||
width: 80,
|
||||
height: 80,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.red.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(40),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.error_outline,
|
||||
color: Colors.red,
|
||||
size: 40,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Text(
|
||||
"Oops! Something went wrong",
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
"Error: $error",
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(fontSize: 14, color: Colors.grey[600]),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
// Retry logic
|
||||
ref.invalidate(productHomePageProvider);
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.blue,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
child: const Text(
|
||||
"Try Again",
|
||||
style: TextStyle(fontWeight: FontWeight.w600),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 🎨 Additional Animation Extensions for Enhanced UI
|
||||
extension AnimatedWidgetExtensions on Widget {
|
||||
Widget fadeInUp({
|
||||
Duration duration = const Duration(milliseconds: 600),
|
||||
Duration delay = Duration.zero,
|
||||
}) {
|
||||
return TweenAnimationBuilder<double>(
|
||||
duration: duration,
|
||||
tween: Tween(begin: 0.0, end: 1.0),
|
||||
builder: (context, value, child) {
|
||||
return Transform.translate(
|
||||
offset: Offset(0, 20 * (1 - value)),
|
||||
child: Opacity(opacity: value, child: this),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget bounceIn({Duration duration = const Duration(milliseconds: 800)}) {
|
||||
return TweenAnimationBuilder<double>(
|
||||
duration: duration,
|
||||
tween: Tween(begin: 0.0, end: 1.0),
|
||||
curve: Curves.elasticOut,
|
||||
builder: (context, value, child) {
|
||||
return Transform.scale(scale: value, child: this);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
import 'package:animated_notch_bottom_bar/animated_notch_bottom_bar/animated_notch_bottom_bar.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:wordpress/view/home_screen.dart';
|
||||
import 'package:wordpress/viewmodel/cart_screen.dart';
|
||||
|
||||
class MainController extends StatefulWidget {
|
||||
const MainController({super.key});
|
||||
|
||||
@override
|
||||
State<MainController> createState() => _MainControllerState();
|
||||
|
||||
// 👇 Add static method to access from children
|
||||
static _MainControllerState? of(BuildContext context) =>
|
||||
context.findAncestorStateOfType<_MainControllerState>();
|
||||
}
|
||||
|
||||
class _MainControllerState extends State<MainController> {
|
||||
final PageController _pageController = PageController(initialPage: 0);
|
||||
|
||||
final NotchBottomBarController _controller = NotchBottomBarController(
|
||||
index: 0,
|
||||
);
|
||||
|
||||
void navigateToTab(int index) {
|
||||
_controller.jumpTo(index);
|
||||
_pageController.jumpToPage(index);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_pageController.dispose();
|
||||
_controller.dispose(); // ✅ prevents AnimationController errors
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
extendBody: true,
|
||||
body: PageView(
|
||||
controller: _pageController,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
children: const [
|
||||
HomeScreen(),
|
||||
CartScreen(),
|
||||
Center(child: Text("👤 Profile Page")),
|
||||
],
|
||||
),
|
||||
bottomNavigationBar: SafeArea(
|
||||
top: false,
|
||||
child: AnimatedNotchBottomBar(
|
||||
notchBottomBarController: _controller,
|
||||
color: Colors.white,
|
||||
notchColor: Colors.blue,
|
||||
showLabel: true,
|
||||
itemLabelStyle: const TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
showShadow: true,
|
||||
elevation: 8,
|
||||
kIconSize: 24,
|
||||
kBottomRadius: 24,
|
||||
removeMargins: false,
|
||||
bottomBarWidth: MediaQuery.of(context).size.width * 0.9,
|
||||
durationInMilliSeconds: 300,
|
||||
bottomBarHeight: 70,
|
||||
showTopRadius: true,
|
||||
showBottomRadius: true,
|
||||
bottomBarItems: const [
|
||||
BottomBarItem(
|
||||
inActiveItem: Icon(Icons.home, color: Colors.grey),
|
||||
activeItem: Icon(Icons.home, color: Colors.white),
|
||||
itemLabel: 'Home',
|
||||
),
|
||||
BottomBarItem(
|
||||
inActiveItem: Icon(Icons.shopping_cart, color: Colors.grey),
|
||||
activeItem: Icon(Icons.shopping_cart, color: Colors.white),
|
||||
itemLabel: 'Cart',
|
||||
),
|
||||
BottomBarItem(
|
||||
inActiveItem: Icon(Icons.person, color: Colors.grey),
|
||||
activeItem: Icon(Icons.person, color: Colors.white),
|
||||
itemLabel: 'Profile',
|
||||
),
|
||||
],
|
||||
onTap: (index) {
|
||||
_pageController.jumpToPage(index);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,325 @@
|
||||
class AllProductModel {
|
||||
final int id;
|
||||
final String name;
|
||||
final String slug;
|
||||
final String permalink;
|
||||
final DateTime dateCreated;
|
||||
final DateTime dateCreatedGmt;
|
||||
final DateTime dateModified;
|
||||
final DateTime dateModifiedGmt;
|
||||
final String type;
|
||||
final String status;
|
||||
final bool featured;
|
||||
final String catalogVisibility;
|
||||
final String description;
|
||||
final String shortDescription;
|
||||
final String sku;
|
||||
final String price;
|
||||
final String? regularPrice;
|
||||
final String? salePrice;
|
||||
final bool onSale;
|
||||
final bool purchasable;
|
||||
final int totalSales;
|
||||
final bool virtual;
|
||||
final bool downloadable;
|
||||
final List<dynamic> downloads;
|
||||
final int downloadLimit;
|
||||
final int downloadExpiry;
|
||||
final String externalUrl;
|
||||
final String buttonText;
|
||||
final String taxStatus;
|
||||
final String taxClass;
|
||||
final bool manageStock;
|
||||
final int? stockQuantity;
|
||||
final String backorders;
|
||||
final bool backordersAllowed;
|
||||
final bool backordered;
|
||||
final int? lowStockAmount;
|
||||
final bool soldIndividually;
|
||||
final String weight;
|
||||
final Dimensions dimensions;
|
||||
final bool shippingRequired;
|
||||
final bool shippingTaxable;
|
||||
final String shippingClass;
|
||||
final int shippingClassId;
|
||||
final bool reviewsAllowed;
|
||||
final String averageRating;
|
||||
final int ratingCount;
|
||||
final List<int> upsellIds;
|
||||
final List<int> crossSellIds;
|
||||
final int parentId;
|
||||
final String purchaseNote;
|
||||
final List<Category> categories;
|
||||
final List<dynamic> brands;
|
||||
final List<Tag> tags;
|
||||
final List<ProductImage> images;
|
||||
final List<Attribute> attributes;
|
||||
final List<dynamic> defaultAttributes;
|
||||
final List<int> variations;
|
||||
final List<dynamic> groupedProducts;
|
||||
final int menuOrder;
|
||||
final String priceHtml;
|
||||
final List<int> relatedIds;
|
||||
final List<MetaData> metaData;
|
||||
final String stockStatus;
|
||||
final bool hasOptions;
|
||||
final String postPassword;
|
||||
final String globalUniqueId;
|
||||
|
||||
AllProductModel({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.slug,
|
||||
required this.permalink,
|
||||
required this.dateCreated,
|
||||
required this.dateCreatedGmt,
|
||||
required this.dateModified,
|
||||
required this.dateModifiedGmt,
|
||||
required this.type,
|
||||
required this.status,
|
||||
required this.featured,
|
||||
required this.catalogVisibility,
|
||||
required this.description,
|
||||
required this.shortDescription,
|
||||
required this.sku,
|
||||
required this.price,
|
||||
this.regularPrice,
|
||||
this.salePrice,
|
||||
required this.onSale,
|
||||
required this.purchasable,
|
||||
required this.totalSales,
|
||||
required this.virtual,
|
||||
required this.downloadable,
|
||||
required this.downloads,
|
||||
required this.downloadLimit,
|
||||
required this.downloadExpiry,
|
||||
required this.externalUrl,
|
||||
required this.buttonText,
|
||||
required this.taxStatus,
|
||||
required this.taxClass,
|
||||
required this.manageStock,
|
||||
this.stockQuantity,
|
||||
required this.backorders,
|
||||
required this.backordersAllowed,
|
||||
required this.backordered,
|
||||
this.lowStockAmount,
|
||||
required this.soldIndividually,
|
||||
required this.weight,
|
||||
required this.dimensions,
|
||||
required this.shippingRequired,
|
||||
required this.shippingTaxable,
|
||||
required this.shippingClass,
|
||||
required this.shippingClassId,
|
||||
required this.reviewsAllowed,
|
||||
required this.averageRating,
|
||||
required this.ratingCount,
|
||||
required this.upsellIds,
|
||||
required this.crossSellIds,
|
||||
required this.parentId,
|
||||
required this.purchaseNote,
|
||||
required this.categories,
|
||||
required this.brands,
|
||||
required this.tags,
|
||||
required this.images,
|
||||
required this.attributes,
|
||||
required this.defaultAttributes,
|
||||
required this.variations,
|
||||
required this.groupedProducts,
|
||||
required this.menuOrder,
|
||||
required this.priceHtml,
|
||||
required this.relatedIds,
|
||||
required this.metaData,
|
||||
required this.stockStatus,
|
||||
required this.hasOptions,
|
||||
required this.postPassword,
|
||||
required this.globalUniqueId,
|
||||
});
|
||||
|
||||
factory AllProductModel.fromJson(Map<String, dynamic> json) => AllProductModel(
|
||||
id: json['id'],
|
||||
name: json['name'],
|
||||
slug: json['slug'],
|
||||
permalink: json['permalink'],
|
||||
dateCreated: DateTime.parse(json['date_created']),
|
||||
dateCreatedGmt: DateTime.parse(json['date_created_gmt']),
|
||||
dateModified: DateTime.parse(json['date_modified']),
|
||||
dateModifiedGmt: DateTime.parse(json['date_modified_gmt']),
|
||||
type: json['type'],
|
||||
status: json['status'],
|
||||
featured: json['featured'],
|
||||
catalogVisibility: json['catalog_visibility'],
|
||||
description: json['description'],
|
||||
shortDescription: json['short_description'],
|
||||
sku: json['sku'],
|
||||
price: json['price'],
|
||||
regularPrice: json['regular_price'],
|
||||
salePrice: json['sale_price'],
|
||||
onSale: json['on_sale'],
|
||||
purchasable: json['purchasable'],
|
||||
totalSales: json['total_sales'],
|
||||
virtual: json['virtual'],
|
||||
downloadable: json['downloadable'],
|
||||
downloads: json['downloads'],
|
||||
downloadLimit: json['download_limit'],
|
||||
downloadExpiry: json['download_expiry'],
|
||||
externalUrl: json['external_url'],
|
||||
buttonText: json['button_text'],
|
||||
taxStatus: json['tax_status'],
|
||||
taxClass: json['tax_class'],
|
||||
manageStock: json['manage_stock'],
|
||||
stockQuantity: json['stock_quantity'],
|
||||
backorders: json['backorders'],
|
||||
backordersAllowed: json['backorders_allowed'],
|
||||
backordered: json['backordered'],
|
||||
lowStockAmount: json['low_stock_amount'],
|
||||
soldIndividually: json['sold_individually'],
|
||||
weight: json['weight'],
|
||||
dimensions: Dimensions.fromJson(json['dimensions']),
|
||||
shippingRequired: json['shipping_required'],
|
||||
shippingTaxable: json['shipping_taxable'],
|
||||
shippingClass: json['shipping_class'],
|
||||
shippingClassId: json['shipping_class_id'],
|
||||
reviewsAllowed: json['reviews_allowed'],
|
||||
averageRating: json['average_rating'],
|
||||
ratingCount: json['rating_count'],
|
||||
upsellIds: List<int>.from(json['upsell_ids']),
|
||||
crossSellIds: List<int>.from(json['cross_sell_ids']),
|
||||
parentId: json['parent_id'],
|
||||
purchaseNote: json['purchase_note'],
|
||||
categories: List<Category>.from(json['categories'].map((x) => Category.fromJson(x))),
|
||||
brands: json['brands'],
|
||||
tags: List<Tag>.from(json['tags'].map((x) => Tag.fromJson(x))),
|
||||
images: List<ProductImage>.from(json['images'].map((x) => ProductImage.fromJson(x))),
|
||||
attributes: List<Attribute>.from(json['attributes'].map((x) => Attribute.fromJson(x))),
|
||||
defaultAttributes: json['default_attributes'],
|
||||
variations: List<int>.from(json['variations']),
|
||||
groupedProducts: json['grouped_products'],
|
||||
menuOrder: json['menu_order'],
|
||||
priceHtml: json['price_html'],
|
||||
relatedIds: List<int>.from(json['related_ids']),
|
||||
metaData: List<MetaData>.from(json['meta_data'].map((x) => MetaData.fromJson(x))),
|
||||
stockStatus: json['stock_status'],
|
||||
hasOptions: json['has_options'],
|
||||
postPassword: json['post_password'],
|
||||
globalUniqueId: json['global_unique_id'],
|
||||
);
|
||||
}
|
||||
|
||||
class Dimensions {
|
||||
final String length;
|
||||
final String width;
|
||||
final String height;
|
||||
|
||||
Dimensions({required this.length, required this.width, required this.height});
|
||||
|
||||
factory Dimensions.fromJson(Map<String, dynamic> json) => Dimensions(
|
||||
length: json['length'],
|
||||
width: json['width'],
|
||||
height: json['height'],
|
||||
);
|
||||
}
|
||||
|
||||
class Category {
|
||||
final int id;
|
||||
final String name;
|
||||
final String slug;
|
||||
|
||||
Category({required this.id, required this.name, required this.slug});
|
||||
|
||||
factory Category.fromJson(Map<String, dynamic> json) => Category(
|
||||
id: json['id'],
|
||||
name: json['name'],
|
||||
slug: json['slug'],
|
||||
);
|
||||
}
|
||||
|
||||
class Tag {
|
||||
final int id;
|
||||
final String name;
|
||||
final String slug;
|
||||
|
||||
Tag({required this.id, required this.name, required this.slug});
|
||||
|
||||
factory Tag.fromJson(Map<String, dynamic> json) => Tag(
|
||||
id: json['id'],
|
||||
name: json['name'],
|
||||
slug: json['slug'],
|
||||
);
|
||||
}
|
||||
|
||||
class ProductImage {
|
||||
final int id;
|
||||
final DateTime dateCreated;
|
||||
final DateTime dateCreatedGmt;
|
||||
final DateTime dateModified;
|
||||
final DateTime dateModifiedGmt;
|
||||
final String src;
|
||||
final String name;
|
||||
final String alt;
|
||||
|
||||
ProductImage({
|
||||
required this.id,
|
||||
required this.dateCreated,
|
||||
required this.dateCreatedGmt,
|
||||
required this.dateModified,
|
||||
required this.dateModifiedGmt,
|
||||
required this.src,
|
||||
required this.name,
|
||||
required this.alt,
|
||||
});
|
||||
|
||||
factory ProductImage.fromJson(Map<String, dynamic> json) => ProductImage(
|
||||
id: json['id'],
|
||||
dateCreated: DateTime.parse(json['date_created']),
|
||||
dateCreatedGmt: DateTime.parse(json['date_created_gmt']),
|
||||
dateModified: DateTime.parse(json['date_modified']),
|
||||
dateModifiedGmt: DateTime.parse(json['date_modified_gmt']),
|
||||
src: json['src'],
|
||||
name: json['name'],
|
||||
alt: json['alt'],
|
||||
);
|
||||
}
|
||||
|
||||
class Attribute {
|
||||
final int id;
|
||||
final String name;
|
||||
final String slug;
|
||||
final int position;
|
||||
final bool visible;
|
||||
final bool variation;
|
||||
final List<String> options;
|
||||
|
||||
Attribute({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.slug,
|
||||
required this.position,
|
||||
required this.visible,
|
||||
required this.variation,
|
||||
required this.options,
|
||||
});
|
||||
|
||||
factory Attribute.fromJson(Map<String, dynamic> json) => Attribute(
|
||||
id: json['id'],
|
||||
name: json['name'],
|
||||
slug: json['slug'],
|
||||
position: json['position'],
|
||||
visible: json['visible'],
|
||||
variation: json['variation'],
|
||||
options: List<String>.from(json['options']),
|
||||
);
|
||||
}
|
||||
|
||||
class MetaData {
|
||||
final int id;
|
||||
final String key;
|
||||
final dynamic value;
|
||||
|
||||
MetaData({required this.id, required this.key, required this.value});
|
||||
|
||||
factory MetaData.fromJson(Map<String, dynamic> json) => MetaData(
|
||||
id: json['id'],
|
||||
key: json['key'],
|
||||
value: json['value'],
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
class CartResponse {
|
||||
final bool success;
|
||||
final Map<String, CartItem> cart;
|
||||
|
||||
CartResponse({required this.success, required this.cart});
|
||||
|
||||
factory CartResponse.fromJson(Map<String, dynamic> json) {
|
||||
final cartMap = <String, CartItem>{};
|
||||
|
||||
if (json['cart'] != null) {
|
||||
if (json['cart'] is Map<String, dynamic>) {
|
||||
// Case 1: cart is a Map
|
||||
(json['cart'] as Map<String, dynamic>).forEach((key, value) {
|
||||
cartMap[key] = CartItem.fromJson(value);
|
||||
});
|
||||
} else if (json['cart'] is List) {
|
||||
// Case 2: cart is a List
|
||||
for (var i = 0; i < (json['cart'] as List).length; i++) {
|
||||
final item = (json['cart'] as List)[i];
|
||||
if (item is Map<String, dynamic>) {
|
||||
cartMap[i.toString()] = CartItem.fromJson(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return CartResponse(success: json['success'] ?? false, cart: cartMap);
|
||||
}
|
||||
}
|
||||
|
||||
class CartItem {
|
||||
final int productId;
|
||||
final int quantity;
|
||||
final String name;
|
||||
final String price;
|
||||
|
||||
CartItem({
|
||||
required this.productId,
|
||||
required this.quantity,
|
||||
required this.name,
|
||||
required this.price,
|
||||
});
|
||||
|
||||
factory CartItem.fromJson(Map<String, dynamic> json) {
|
||||
return CartItem(
|
||||
productId: int.tryParse(json['product_id']?.toString() ?? '0') ?? 0,
|
||||
quantity: int.tryParse(json['quantity']?.toString() ?? '0') ?? 0,
|
||||
name: json['name']?.toString() ?? '',
|
||||
price: json['price']?.toString() ?? '0',
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'product_id': productId,
|
||||
'quantity': quantity,
|
||||
'name': name,
|
||||
'price': price,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,661 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:wordpress/providers/api_controller.dart';
|
||||
import 'package:wordpress/providers/api_repositoryprovider.dart';
|
||||
import 'package:wordpress/utils/ftaf.dart';
|
||||
import 'package:wordpress/view/check_out_screen.dart';
|
||||
import 'package:wordpress/viewmodel/cart_model.dart';
|
||||
|
||||
class CartScreen extends ConsumerWidget {
|
||||
const CartScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final cartAsync = ref.watch(cartProvider);
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.grey.shade50,
|
||||
appBar: AppBar(
|
||||
title: const Text(
|
||||
"My Cart",
|
||||
style: TextStyle(
|
||||
color: Colors.black87,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 20,
|
||||
),
|
||||
),
|
||||
backgroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
shadowColor: Colors.black12,
|
||||
iconTheme: const IconThemeData(color: Colors.black87),
|
||||
systemOverlayStyle: SystemUiOverlayStyle.dark,
|
||||
actions: [
|
||||
cartAsync.when(
|
||||
data: (cartResponse) => cartResponse.cart.isNotEmpty
|
||||
? Container(
|
||||
margin: const EdgeInsets.only(right: 16),
|
||||
child: Center(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 6,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.orange.shade100,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Text(
|
||||
"${cartResponse.cart.length} items",
|
||||
style: TextStyle(
|
||||
color: Colors.orange.shade800,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
: const SizedBox(),
|
||||
loading: () => const SizedBox(),
|
||||
error: (_, __) => const SizedBox(),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: cartAsync.when(
|
||||
data: (cartResponse) {
|
||||
if (cartResponse.cart.isEmpty) {
|
||||
return _buildEmptyCart(context);
|
||||
}
|
||||
|
||||
final items = cartResponse.cart.values.toList();
|
||||
|
||||
return SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
// Header with item count
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 20,
|
||||
vertical: 16,
|
||||
),
|
||||
color: Colors.white,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
"Review your order",
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.grey.shade800,
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 6,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.orange.shade100,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Text(
|
||||
"${items.length} items",
|
||||
style: TextStyle(
|
||||
color: Colors.orange.shade800,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 8),
|
||||
|
||||
// Cart Items
|
||||
ListView.separated(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemCount: items.length,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 8,
|
||||
),
|
||||
separatorBuilder: (context, index) =>
|
||||
const SizedBox(height: 12),
|
||||
itemBuilder: (context, index) {
|
||||
final item = items[index];
|
||||
return AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: _buildCartItem(context, item, ref),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Cart Summary
|
||||
_buildCartSummary(context, items, ref),
|
||||
|
||||
// Add extra space for bottom navigation
|
||||
const SizedBox(height: 100),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
loading: () => Container(
|
||||
color: Colors.white,
|
||||
child: const Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
CircularProgressIndicator(
|
||||
valueColor: AlwaysStoppedAnimation<Color>(Colors.orange),
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
Text(
|
||||
"Loading your cart...",
|
||||
style: TextStyle(fontSize: 16, color: Colors.grey),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
error: (err, stack) => Container(
|
||||
color: Colors.white,
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.error_outline, size: 64, color: Colors.red.shade300),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
"Something went wrong",
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.grey.shade800,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
"Please try again later",
|
||||
style: TextStyle(fontSize: 14, color: Colors.grey.shade600),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
ElevatedButton(
|
||||
onPressed: () => ref.invalidate(cartProvider),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.orange,
|
||||
foregroundColor: Colors.white,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
child: const Text("Retry"),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEmptyCart(BuildContext context) {
|
||||
return Container(
|
||||
color: Colors.white,
|
||||
width: double.infinity,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(24),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.orange.shade50,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(
|
||||
Icons.shopping_cart_outlined,
|
||||
size: 80,
|
||||
color: Colors.orange.shade300,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
"Your cart is empty",
|
||||
style: TextStyle(
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.grey.shade800,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
"Add items to get started",
|
||||
style: TextStyle(fontSize: 16, color: Colors.grey.shade600),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
ElevatedButton.icon(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
icon: const Icon(Icons.shopping_bag_outlined),
|
||||
label: const Text("Continue Shopping"),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.orange,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(25),
|
||||
),
|
||||
elevation: 2,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCartItem(BuildContext context, CartItem item, WidgetRef ref) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade50,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: Colors.grey.shade200),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Row(
|
||||
children: [
|
||||
// Product Image/Icon
|
||||
Container(
|
||||
width: 70,
|
||||
height: 70,
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [Colors.orange.shade100, Colors.orange.shade50],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Icon(
|
||||
Icons.shopping_bag_outlined,
|
||||
size: 32,
|
||||
color: Colors.orange.shade600,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
|
||||
// Product Info
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Product Name and Delete Button
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
item.name,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.black87,
|
||||
),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
GestureDetector(
|
||||
onTap: () async {
|
||||
// Show confirmation dialog
|
||||
final confirm = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
title: const Text("Remove Item"),
|
||||
content: Text("Remove ${item.name} from cart?"),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () =>
|
||||
Navigator.of(context).pop(false),
|
||||
child: const Text("Cancel"),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () =>
|
||||
Navigator.of(context).pop(true),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.red,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
child: const Text("Remove"),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
if (confirm == true) {
|
||||
try {
|
||||
final apiRepo = ref.read(apiRepositoryProvider);
|
||||
await apiRepo.removeItem(
|
||||
productId: item.productId,
|
||||
quantity: item.quantity,
|
||||
);
|
||||
ref.invalidate(cartProvider);
|
||||
} catch (e) {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text("Failed to remove item: $e"),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(6),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.red.shade50,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Icon(
|
||||
Icons.delete_outline,
|
||||
size: 20,
|
||||
color: Colors.red.shade400,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Price and Quantity Row
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
// Price
|
||||
Text(
|
||||
"₹${((double.tryParse(item.price) ?? 0) * item.quantity).toStringAsFixed(2)}",
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: Colors.orange.shade700,
|
||||
),
|
||||
),
|
||||
|
||||
// Quantity Controls
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(25),
|
||||
border: Border.all(color: Colors.grey.shade300),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_buildQuantityButton(
|
||||
icon: Icons.remove,
|
||||
onTap: () async {
|
||||
try {
|
||||
final apiRepo = ref.read(
|
||||
apiRepositoryProvider,
|
||||
);
|
||||
if (item.quantity > 1) {
|
||||
await apiRepo.removeItem(
|
||||
productId: item.productId,
|
||||
quantity: 1,
|
||||
);
|
||||
} else {
|
||||
await apiRepo.removeItem(
|
||||
productId: item.productId,
|
||||
quantity: item.quantity,
|
||||
);
|
||||
}
|
||||
ref.invalidate(cartProvider);
|
||||
} catch (e) {
|
||||
print("Error decreasing quantity: $e");
|
||||
}
|
||||
},
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
),
|
||||
child: Text(
|
||||
"${item.quantity}",
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
_buildQuantityButton(
|
||||
icon: Icons.add,
|
||||
onTap: () async {
|
||||
try {
|
||||
await ref
|
||||
.read(apiRepositoryProvider)
|
||||
.addItem(
|
||||
productId: item.productId,
|
||||
quantity: 1,
|
||||
);
|
||||
ref.invalidate(cartProvider);
|
||||
} catch (e) {
|
||||
print("Error increasing quantity: $e");
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildQuantityButton({
|
||||
required IconData icon,
|
||||
required VoidCallback onTap,
|
||||
}) {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade50,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Icon(icon, size: 18, color: Colors.grey.shade700),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCartSummary(
|
||||
BuildContext context,
|
||||
List<CartItem> items,
|
||||
WidgetRef ref,
|
||||
) {
|
||||
double subtotal = 0;
|
||||
int totalItems = 0;
|
||||
|
||||
for (var item in items) {
|
||||
subtotal += (double.tryParse(item.price) ?? 0) * item.quantity;
|
||||
totalItems += item.quantity;
|
||||
}
|
||||
|
||||
final shipping = subtotal > 500 ? 0.0 : 50.0;
|
||||
final tax = subtotal * 0.18; // 18% GST
|
||||
final total = subtotal + shipping + tax;
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16),
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: Colors.grey.shade200),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Summary Header
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Text(
|
||||
"Order Summary",
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
"$totalItems items",
|
||||
style: TextStyle(fontSize: 14, color: Colors.grey.shade600),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Price Breakdown
|
||||
_buildPriceRow("Subtotal", subtotal),
|
||||
const SizedBox(height: 8),
|
||||
_buildPriceRow(
|
||||
"Shipping",
|
||||
shipping,
|
||||
subtitle: shipping == 0 ? "Free shipping!" : null,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
_buildPriceRow("GST (18%)", tax),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
const Divider(thickness: 1),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Total
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Text(
|
||||
"Total",
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
"₹${total.toStringAsFixed(2)}",
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: Colors.orange.shade700,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Checkout Button
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
height: 56,
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text("Proceeding to checkout..."),
|
||||
backgroundColor: Colors.green,
|
||||
),
|
||||
);
|
||||
Get.to(CheckOutScreen());
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.orange,
|
||||
foregroundColor: Colors.white,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
elevation: 3,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Icons.shopping_bag),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
"Proceed to Checkout",
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 30),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPriceRow(String label, double amount, {String? subtitle}) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(fontSize: 14, color: Colors.grey.shade700),
|
||||
),
|
||||
if (subtitle != null)
|
||||
Text(
|
||||
subtitle,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.green.shade600,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Text(
|
||||
amount == 0 ? "FREE" : "₹${amount.toStringAsFixed(2)}",
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: amount == 0 ? Colors.green.shade600 : Colors.black87,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,427 @@
|
||||
import 'dart:convert';
|
||||
|
||||
DetailModel detailModelFromJson(String str) =>
|
||||
DetailModel.fromJson(json.decode(str));
|
||||
|
||||
String detailModelToJson(DetailModel data) => json.encode(data.toJson());
|
||||
|
||||
class DetailModel {
|
||||
final int? id;
|
||||
final String? name;
|
||||
final String? slug;
|
||||
final String? permalink;
|
||||
final String? dateCreated;
|
||||
final String? dateCreatedGmt;
|
||||
final String? dateModified;
|
||||
final String? dateModifiedGmt;
|
||||
final String? type;
|
||||
final String? status;
|
||||
final bool? featured;
|
||||
final String? catalogVisibility;
|
||||
final String? description;
|
||||
final String? shortDescription;
|
||||
final String? sku;
|
||||
final String? price;
|
||||
final String? regularPrice;
|
||||
final String? salePrice;
|
||||
final bool? onSale;
|
||||
final bool? purchasable;
|
||||
final int? totalSales;
|
||||
final bool? virtual;
|
||||
final bool? downloadable;
|
||||
final int? downloadLimit;
|
||||
final int? downloadExpiry;
|
||||
final String? externalUrl;
|
||||
final String? buttonText;
|
||||
final String? taxStatus;
|
||||
final String? taxClass;
|
||||
final bool? manageStock;
|
||||
final int? stockQuantity;
|
||||
final String? backorders;
|
||||
final bool? backordersAllowed;
|
||||
final bool? backordered;
|
||||
final bool? soldIndividually;
|
||||
final String? weight;
|
||||
final Dimensions? dimensions;
|
||||
final bool? shippingRequired;
|
||||
final bool? shippingTaxable;
|
||||
final String? shippingClass;
|
||||
final int? shippingClassId;
|
||||
final bool? reviewsAllowed;
|
||||
final String? averageRating;
|
||||
final int? ratingCount;
|
||||
final int? parentId;
|
||||
final String? purchaseNote;
|
||||
final List<Category>? categories;
|
||||
final List<dynamic>? brands;
|
||||
final List<Tag>? tags;
|
||||
final List<ImageModel>? images;
|
||||
final List<Attribute>? attributes;
|
||||
final List<dynamic>? defaultAttributes;
|
||||
final List<int>? variations;
|
||||
final List<dynamic>? groupedProducts;
|
||||
final int? menuOrder;
|
||||
final String? priceHtml;
|
||||
final List<int>? relatedIds;
|
||||
final List<MetaData>? metaData;
|
||||
final String? stockStatus;
|
||||
final bool? hasOptions;
|
||||
final String? postPassword;
|
||||
final String? globalUniqueId;
|
||||
|
||||
DetailModel({
|
||||
this.id,
|
||||
this.name,
|
||||
this.slug,
|
||||
this.permalink,
|
||||
this.dateCreated,
|
||||
this.dateCreatedGmt,
|
||||
this.dateModified,
|
||||
this.dateModifiedGmt,
|
||||
this.type,
|
||||
this.status,
|
||||
this.featured,
|
||||
this.catalogVisibility,
|
||||
this.description,
|
||||
this.shortDescription,
|
||||
this.sku,
|
||||
this.price,
|
||||
this.regularPrice,
|
||||
this.salePrice,
|
||||
this.onSale,
|
||||
this.purchasable,
|
||||
this.totalSales,
|
||||
this.virtual,
|
||||
this.downloadable,
|
||||
this.downloadLimit,
|
||||
this.downloadExpiry,
|
||||
this.externalUrl,
|
||||
this.buttonText,
|
||||
this.taxStatus,
|
||||
this.taxClass,
|
||||
this.manageStock,
|
||||
this.stockQuantity,
|
||||
this.backorders,
|
||||
this.backordersAllowed,
|
||||
this.backordered,
|
||||
this.soldIndividually,
|
||||
this.weight,
|
||||
this.dimensions,
|
||||
this.shippingRequired,
|
||||
this.shippingTaxable,
|
||||
this.shippingClass,
|
||||
this.shippingClassId,
|
||||
this.reviewsAllowed,
|
||||
this.averageRating,
|
||||
this.ratingCount,
|
||||
this.parentId,
|
||||
this.purchaseNote,
|
||||
this.categories,
|
||||
this.brands,
|
||||
this.tags,
|
||||
this.images,
|
||||
this.attributes,
|
||||
this.defaultAttributes,
|
||||
this.variations,
|
||||
this.groupedProducts,
|
||||
this.menuOrder,
|
||||
this.priceHtml,
|
||||
this.relatedIds,
|
||||
this.metaData,
|
||||
this.stockStatus,
|
||||
this.hasOptions,
|
||||
this.postPassword,
|
||||
this.globalUniqueId,
|
||||
});
|
||||
|
||||
factory DetailModel.fromJson(Map<String, dynamic> json) => DetailModel(
|
||||
id: json["id"],
|
||||
name: json["name"],
|
||||
slug: json["slug"],
|
||||
permalink: json["permalink"],
|
||||
dateCreated: json["date_created"],
|
||||
dateCreatedGmt: json["date_created_gmt"],
|
||||
dateModified: json["date_modified"],
|
||||
dateModifiedGmt: json["date_modified_gmt"],
|
||||
type: json["type"],
|
||||
status: json["status"],
|
||||
featured: json["featured"],
|
||||
catalogVisibility: json["catalog_visibility"],
|
||||
description: json["description"],
|
||||
shortDescription: json["short_description"],
|
||||
sku: json["sku"],
|
||||
price: json["price"],
|
||||
regularPrice: json["regular_price"],
|
||||
salePrice: json["sale_price"],
|
||||
onSale: json["on_sale"],
|
||||
purchasable: json["purchasable"],
|
||||
totalSales: json["total_sales"],
|
||||
virtual: json["virtual"],
|
||||
downloadable: json["downloadable"],
|
||||
downloadLimit: json["download_limit"],
|
||||
downloadExpiry: json["download_expiry"],
|
||||
externalUrl: json["external_url"],
|
||||
buttonText: json["button_text"],
|
||||
taxStatus: json["tax_status"],
|
||||
taxClass: json["tax_class"],
|
||||
manageStock: json["manage_stock"],
|
||||
stockQuantity: json["stock_quantity"],
|
||||
backorders: json["backorders"],
|
||||
backordersAllowed: json["backorders_allowed"],
|
||||
backordered: json["backordered"],
|
||||
soldIndividually: json["sold_individually"],
|
||||
weight: json["weight"],
|
||||
dimensions: json["dimensions"] != null
|
||||
? Dimensions.fromJson(json["dimensions"])
|
||||
: null,
|
||||
shippingRequired: json["shipping_required"],
|
||||
shippingTaxable: json["shipping_taxable"],
|
||||
shippingClass: json["shipping_class"],
|
||||
shippingClassId: json["shipping_class_id"],
|
||||
reviewsAllowed: json["reviews_allowed"],
|
||||
averageRating: json["average_rating"],
|
||||
ratingCount: json["rating_count"],
|
||||
parentId: json["parent_id"],
|
||||
purchaseNote: json["purchase_note"],
|
||||
categories: json["categories"] == null
|
||||
? []
|
||||
: List<Category>.from(
|
||||
json["categories"].map((x) => Category.fromJson(x)),
|
||||
),
|
||||
brands: json["brands"] ?? [],
|
||||
tags: json["tags"] == null
|
||||
? []
|
||||
: List<Tag>.from(json["tags"].map((x) => Tag.fromJson(x))),
|
||||
images: json["images"] == null
|
||||
? []
|
||||
: List<ImageModel>.from(
|
||||
json["images"].map((x) => ImageModel.fromJson(x)),
|
||||
),
|
||||
attributes: json["attributes"] == null
|
||||
? []
|
||||
: List<Attribute>.from(
|
||||
json["attributes"].map((x) => Attribute.fromJson(x)),
|
||||
),
|
||||
defaultAttributes: json["default_attributes"] ?? [],
|
||||
variations: json["variations"] == null
|
||||
? []
|
||||
: List<int>.from(json["variations"].map((x) => x)),
|
||||
groupedProducts: json["grouped_products"] ?? [],
|
||||
menuOrder: json["menu_order"],
|
||||
priceHtml: json["price_html"],
|
||||
relatedIds: json["related_ids"] == null
|
||||
? []
|
||||
: List<int>.from(json["related_ids"].map((x) => x)),
|
||||
metaData: json["meta_data"] == null
|
||||
? []
|
||||
: List<MetaData>.from(
|
||||
json["meta_data"].map((x) => MetaData.fromJson(x)),
|
||||
),
|
||||
stockStatus: json["stock_status"],
|
||||
hasOptions: json["has_options"],
|
||||
postPassword: json["post_password"],
|
||||
globalUniqueId: json["global_unique_id"],
|
||||
);
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
"id": id,
|
||||
"name": name,
|
||||
"slug": slug,
|
||||
"permalink": permalink,
|
||||
"date_created": dateCreated,
|
||||
"date_created_gmt": dateCreatedGmt,
|
||||
"date_modified": dateModified,
|
||||
"date_modified_gmt": dateModifiedGmt,
|
||||
"type": type,
|
||||
"status": status,
|
||||
"featured": featured,
|
||||
"catalog_visibility": catalogVisibility,
|
||||
"description": description,
|
||||
"short_description": shortDescription,
|
||||
"sku": sku,
|
||||
"price": price,
|
||||
"regular_price": regularPrice,
|
||||
"sale_price": salePrice,
|
||||
"on_sale": onSale,
|
||||
"purchasable": purchasable,
|
||||
"total_sales": totalSales,
|
||||
"virtual": virtual,
|
||||
"downloadable": downloadable,
|
||||
"download_limit": downloadLimit,
|
||||
"download_expiry": downloadExpiry,
|
||||
"external_url": externalUrl,
|
||||
"button_text": buttonText,
|
||||
"tax_status": taxStatus,
|
||||
"tax_class": taxClass,
|
||||
"manage_stock": manageStock,
|
||||
"stock_quantity": stockQuantity,
|
||||
"backorders": backorders,
|
||||
"backorders_allowed": backordersAllowed,
|
||||
"backordered": backordered,
|
||||
"sold_individually": soldIndividually,
|
||||
"weight": weight,
|
||||
"dimensions": dimensions?.toJson(),
|
||||
"shipping_required": shippingRequired,
|
||||
"shipping_taxable": shippingTaxable,
|
||||
"shipping_class": shippingClass,
|
||||
"shipping_class_id": shippingClassId,
|
||||
"reviews_allowed": reviewsAllowed,
|
||||
"average_rating": averageRating,
|
||||
"rating_count": ratingCount,
|
||||
"parent_id": parentId,
|
||||
"purchase_note": purchaseNote,
|
||||
"categories": categories?.map((x) => x.toJson()).toList(),
|
||||
"brands": brands,
|
||||
"tags": tags?.map((x) => x.toJson()).toList(),
|
||||
"images": images?.map((x) => x.toJson()).toList(),
|
||||
"attributes": attributes?.map((x) => x.toJson()).toList(),
|
||||
"default_attributes": defaultAttributes,
|
||||
"variations": variations,
|
||||
"grouped_products": groupedProducts,
|
||||
"menu_order": menuOrder,
|
||||
"price_html": priceHtml,
|
||||
"related_ids": relatedIds,
|
||||
"meta_data": metaData?.map((x) => x.toJson()).toList(),
|
||||
"stock_status": stockStatus,
|
||||
"has_options": hasOptions,
|
||||
"post_password": postPassword,
|
||||
"global_unique_id": globalUniqueId,
|
||||
};
|
||||
}
|
||||
|
||||
class Dimensions {
|
||||
final String? length;
|
||||
final String? width;
|
||||
final String? height;
|
||||
|
||||
Dimensions({this.length, this.width, this.height});
|
||||
|
||||
factory Dimensions.fromJson(Map<String, dynamic> json) => Dimensions(
|
||||
length: json["length"],
|
||||
width: json["width"],
|
||||
height: json["height"],
|
||||
);
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
"length": length,
|
||||
"width": width,
|
||||
"height": height,
|
||||
};
|
||||
}
|
||||
|
||||
class Category {
|
||||
final int? id;
|
||||
final String? name;
|
||||
final String? slug;
|
||||
|
||||
Category({this.id, this.name, this.slug});
|
||||
|
||||
factory Category.fromJson(Map<String, dynamic> json) =>
|
||||
Category(id: json["id"], name: json["name"], slug: json["slug"]);
|
||||
|
||||
Map<String, dynamic> toJson() => {"id": id, "name": name, "slug": slug};
|
||||
}
|
||||
|
||||
class Tag {
|
||||
final int? id;
|
||||
final String? name;
|
||||
final String? slug;
|
||||
|
||||
Tag({this.id, this.name, this.slug});
|
||||
|
||||
factory Tag.fromJson(Map<String, dynamic> json) =>
|
||||
Tag(id: json["id"], name: json["name"], slug: json["slug"]);
|
||||
|
||||
Map<String, dynamic> toJson() => {"id": id, "name": name, "slug": slug};
|
||||
}
|
||||
|
||||
class ImageModel {
|
||||
final int? id;
|
||||
final String? dateCreated;
|
||||
final String? dateModified;
|
||||
final String? src;
|
||||
final String? name;
|
||||
final String? alt;
|
||||
|
||||
ImageModel({
|
||||
this.id,
|
||||
this.dateCreated,
|
||||
this.dateModified,
|
||||
this.src,
|
||||
this.name,
|
||||
this.alt,
|
||||
});
|
||||
|
||||
factory ImageModel.fromJson(Map<String, dynamic> json) => ImageModel(
|
||||
id: json["id"],
|
||||
dateCreated: json["date_created"],
|
||||
dateModified: json["date_modified"],
|
||||
src: json["src"],
|
||||
name: json["name"],
|
||||
alt: json["alt"],
|
||||
);
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
"id": id,
|
||||
"date_created": dateCreated,
|
||||
"date_modified": dateModified,
|
||||
"src": src,
|
||||
"name": name,
|
||||
"alt": alt,
|
||||
};
|
||||
}
|
||||
|
||||
class Attribute {
|
||||
final int? id;
|
||||
final String? name;
|
||||
final String? slug;
|
||||
final int? position;
|
||||
final bool? visible;
|
||||
final bool? variation;
|
||||
final List<String>? options;
|
||||
|
||||
Attribute({
|
||||
this.id,
|
||||
this.name,
|
||||
this.slug,
|
||||
this.position,
|
||||
this.visible,
|
||||
this.variation,
|
||||
this.options,
|
||||
});
|
||||
|
||||
factory Attribute.fromJson(Map<String, dynamic> json) => Attribute(
|
||||
id: json["id"],
|
||||
name: json["name"],
|
||||
slug: json["slug"],
|
||||
position: json["position"],
|
||||
visible: json["visible"],
|
||||
variation: json["variation"],
|
||||
options: json["options"] == null
|
||||
? []
|
||||
: List<String>.from(json["options"].map((x) => x)),
|
||||
);
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
"id": id,
|
||||
"name": name,
|
||||
"slug": slug,
|
||||
"position": position,
|
||||
"visible": visible,
|
||||
"variation": variation,
|
||||
"options": options,
|
||||
};
|
||||
}
|
||||
|
||||
class MetaData {
|
||||
final int? id;
|
||||
final String? key;
|
||||
final dynamic value;
|
||||
|
||||
MetaData({this.id, this.key, this.value});
|
||||
|
||||
factory MetaData.fromJson(Map<String, dynamic> json) =>
|
||||
MetaData(id: json["id"], key: json["key"], value: json["value"]);
|
||||
|
||||
Map<String, dynamic> toJson() => {"id": id, "key": key, "value": value};
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
class ProductHomePageModel {
|
||||
final List<BannerModel> banners;
|
||||
final List<CategoryModel> categories;
|
||||
final List<ProductModel> featured;
|
||||
final List<ProductModel> latest;
|
||||
|
||||
ProductHomePageModel({
|
||||
required this.banners,
|
||||
required this.categories,
|
||||
required this.featured,
|
||||
required this.latest,
|
||||
});
|
||||
|
||||
factory ProductHomePageModel.fromJson(Map<String, dynamic> json) {
|
||||
return ProductHomePageModel(
|
||||
banners: (json['banners'] is List)
|
||||
? (json['banners'] as List)
|
||||
.map((e) => BannerModel.fromJson(e))
|
||||
.toList()
|
||||
: [],
|
||||
categories: (json['categories'] is List)
|
||||
? (json['categories'] as List)
|
||||
.map((e) => CategoryModel.fromJson(e))
|
||||
.toList()
|
||||
: [],
|
||||
featured: (json['featured_products'] is List)
|
||||
? (json['featured_products'] as List)
|
||||
.map((e) => ProductModel.fromJson(e))
|
||||
.toList()
|
||||
: [],
|
||||
latest: (json['latest_products'] is List)
|
||||
? (json['latest_products'] as List)
|
||||
.map((e) => ProductModel.fromJson(e))
|
||||
.toList()
|
||||
: [],
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'banners': banners.map((e) => e.toJson()).toList(),
|
||||
'categories': categories.map((e) => e.toJson()).toList(),
|
||||
'featured_products': featured.map((e) => e.toJson()).toList(),
|
||||
'latest_products': latest.map((e) => e.toJson()).toList(),
|
||||
};
|
||||
}
|
||||
|
||||
/// Banner Model
|
||||
class BannerModel {
|
||||
final int id;
|
||||
final String image;
|
||||
|
||||
BannerModel({required this.id, required this.image});
|
||||
|
||||
factory BannerModel.fromJson(Map<String, dynamic> json) {
|
||||
return BannerModel(
|
||||
id: json['id'] ?? 0,
|
||||
image: (json['image'] is String) ? json['image'] : '',
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() => {'id': id, 'image': image};
|
||||
}
|
||||
|
||||
/// Category Model
|
||||
class CategoryModel {
|
||||
final int id;
|
||||
final String name;
|
||||
final String slug;
|
||||
final String? image; // can be null/false
|
||||
|
||||
CategoryModel({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.slug,
|
||||
this.image,
|
||||
});
|
||||
|
||||
factory CategoryModel.fromJson(Map<String, dynamic> json) {
|
||||
return CategoryModel(
|
||||
id: json['id'] ?? 0,
|
||||
name: (json['name'] is String) ? json['name'] : '',
|
||||
slug: (json['slug'] is String) ? json['slug'] : '',
|
||||
image: (json['image'] is String) ? json['image'] : null,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'id': id,
|
||||
'name': name,
|
||||
'slug': slug,
|
||||
'image': image,
|
||||
};
|
||||
}
|
||||
|
||||
/// Product Model (for featured & latest)
|
||||
class ProductModel {
|
||||
final int id;
|
||||
final String name;
|
||||
final String price;
|
||||
final String image;
|
||||
|
||||
ProductModel({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.price,
|
||||
required this.image,
|
||||
});
|
||||
|
||||
factory ProductModel.fromJson(Map<String, dynamic> json) {
|
||||
return ProductModel(
|
||||
id: json['id'] ?? 0,
|
||||
name: (json['name'] is String) ? json['name'] : '',
|
||||
price: (json['price'] is String) ? json['price'] : '',
|
||||
image: (json['image'] is String) ? json['image'] : '',
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'id': id,
|
||||
'name': name,
|
||||
'price': price,
|
||||
'image': image,
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user