Products page connected to the backend.
This commit is contained in:
parent
595554d541
commit
a978699dfc
@ -23,4 +23,7 @@ class ApiEndpoints {
|
|||||||
|
|
||||||
///Brands
|
///Brands
|
||||||
static const String brands = "/v1/brands";
|
static const String brands = "/v1/brands";
|
||||||
|
|
||||||
|
///Products
|
||||||
|
static const product = '/api/brands/';
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import 'package:autos/presentation/screens/auth/sign_up_screen.dart';
|
|||||||
import 'package:autos/presentation/screens/brands/brands_screen.dart';
|
import 'package:autos/presentation/screens/brands/brands_screen.dart';
|
||||||
import 'package:autos/presentation/screens/dashboard/dashboard_screen.dart';
|
import 'package:autos/presentation/screens/dashboard/dashboard_screen.dart';
|
||||||
import 'package:autos/presentation/screens/ebay/ebay_screen.dart';
|
import 'package:autos/presentation/screens/ebay/ebay_screen.dart';
|
||||||
|
import 'package:autos/presentation/screens/products/products_screen.dart';
|
||||||
import 'package:autos/presentation/screens/store/create_location_screen.dart';
|
import 'package:autos/presentation/screens/store/create_location_screen.dart';
|
||||||
import 'package:autos/presentation/screens/store/store.dart';
|
import 'package:autos/presentation/screens/store/store.dart';
|
||||||
import 'package:autos/presentation/screens/turn14_screen/turn14_screen.dart';
|
import 'package:autos/presentation/screens/turn14_screen/turn14_screen.dart';
|
||||||
@ -61,6 +62,9 @@ class AppRouter {
|
|||||||
case AppRoutePaths.brands:
|
case AppRoutePaths.brands:
|
||||||
return slideRoute(BrandsScreen());
|
return slideRoute(BrandsScreen());
|
||||||
|
|
||||||
|
case AppRoutePaths.products:
|
||||||
|
return slideRoute(ProductsScreen());
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return _defaultFallback(settings);
|
return _defaultFallback(settings);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,11 +1,15 @@
|
|||||||
class AppRoutePaths {
|
class AppRoutePaths {
|
||||||
|
///Auth
|
||||||
static const auth = '/auth';
|
static const auth = '/auth';
|
||||||
static const signup = '/signup';
|
static const signup = '/signup';
|
||||||
static const forgotPassword = '/forgotPassword';
|
static const forgotPassword = '/forgotPassword';
|
||||||
|
|
||||||
|
///Screens
|
||||||
static const dashboard = '/dashboard';
|
static const dashboard = '/dashboard';
|
||||||
static const turn14 = '/turn14';
|
static const turn14 = '/turn14';
|
||||||
static const ebay = '/ebay';
|
static const ebay = '/ebay';
|
||||||
static const store = '/store';
|
static const store = '/store';
|
||||||
static const createStoreLocation = '/createStoreLocation';
|
static const createStoreLocation = '/createStoreLocation';
|
||||||
static const brands = '/brands';
|
static const brands = '/brands';
|
||||||
|
static const products = '/products';
|
||||||
}
|
}
|
||||||
|
|||||||
@ -65,7 +65,7 @@ class SideMenu extends ConsumerWidget {
|
|||||||
// --- MANAGE ---
|
// --- MANAGE ---
|
||||||
_sectionHeader("MANAGE"),
|
_sectionHeader("MANAGE"),
|
||||||
_menuItem(context, "🏷️", "Brands", AppRoutePaths.brands),
|
_menuItem(context, "🏷️", "Brands", AppRoutePaths.brands),
|
||||||
_menuItem(context, "📦", "Products", 'products'),
|
_menuItem(context, "📦", "Products", AppRoutePaths.products),
|
||||||
_menuItem(context, "⬇️", "Imports", 'imports'),
|
_menuItem(context, "⬇️", "Imports", 'imports'),
|
||||||
|
|
||||||
// --- ACCOUNT ---
|
// --- ACCOUNT ---
|
||||||
|
|||||||
21
lib/data/models/product_model.dart
Normal file
21
lib/data/models/product_model.dart
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
|
||||||
|
|
||||||
|
import 'package:autos/domain/entities/product.dart';
|
||||||
|
|
||||||
|
class ProductModel extends ProductEntity {
|
||||||
|
ProductModel({
|
||||||
|
required super.id,
|
||||||
|
required super.name,
|
||||||
|
required super.image,
|
||||||
|
required super.price,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory ProductModel.fromJson(Map<String, dynamic> json) {
|
||||||
|
return ProductModel(
|
||||||
|
id: json['id'],
|
||||||
|
name: json['name'],
|
||||||
|
image: json['image'],
|
||||||
|
price: (json['price'] as num).toDouble(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -20,7 +20,7 @@ class BrandsRepositoryImpl implements BrandsRepository {
|
|||||||
BrandsRepositoryImpl(this._apiService);
|
BrandsRepositoryImpl(this._apiService);
|
||||||
|
|
||||||
/// ✅ ALWAYS CALL TOKEN API (FORCE REFRESH)
|
/// ✅ ALWAYS CALL TOKEN API (FORCE REFRESH)
|
||||||
Future<String> getTurn14AccessToken() async {
|
Future<String> getTurn14AccessToken() async {
|
||||||
final jsonString = await _storage.read(key: _userKey);
|
final jsonString = await _storage.read(key: _userKey);
|
||||||
if (jsonString == null) {
|
if (jsonString == null) {
|
||||||
throw Exception("User not found. Please login again.");
|
throw Exception("User not found. Please login again.");
|
||||||
@ -52,7 +52,7 @@ Future<String> getTurn14AccessToken() async {
|
|||||||
|
|
||||||
debugPrint("Turn14 Token: $token");
|
debugPrint("Turn14 Token: $token");
|
||||||
return token;
|
return token;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// ✅ FETCH BRANDS USING FRESH TOKEN EVERY TIME
|
/// ✅ FETCH BRANDS USING FRESH TOKEN EVERY TIME
|
||||||
@override
|
@override
|
||||||
@ -88,4 +88,38 @@ Future<String> getTurn14AccessToken() async {
|
|||||||
throw Exception("Unexpected error: $e");
|
throw Exception("Unexpected error: $e");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// ✅ BULK INSERT SELECTED BRANDS
|
||||||
|
Future<Map<String, dynamic>> saveSelectedBrands({
|
||||||
|
required String userId,
|
||||||
|
required List<BrandEntity> brands,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
final payload = {
|
||||||
|
"userid": userId,
|
||||||
|
"brands": brands
|
||||||
|
.map(
|
||||||
|
(brand) => {
|
||||||
|
"id": brand.id.toString(),
|
||||||
|
"name": brand.name,
|
||||||
|
"logo": brand.logo,
|
||||||
|
"dropship": brand.dropship,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
};
|
||||||
|
|
||||||
|
final response = await _apiService.postWithOptions(
|
||||||
|
"/api/brands/bulk-insert",
|
||||||
|
overrideBaseUrl: "https://ebay.backend.data4autos.com",
|
||||||
|
data: payload,
|
||||||
|
);
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
} on DioException catch (e) {
|
||||||
|
throw Exception("Save failed: ${e.response?.data ?? e.message}");
|
||||||
|
} catch (e) {
|
||||||
|
throw Exception("Unexpected error: $e");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
65
lib/data/repositories/product_repository.dart
Normal file
65
lib/data/repositories/product_repository.dart
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
import 'package:autos/core/constants/api_endpoints.dart';
|
||||||
|
import 'package:autos/data/models/product_model.dart';
|
||||||
|
import 'package:autos/data/sources/remote/api_service.dart';
|
||||||
|
import 'package:autos/domain/entities/product.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
abstract class ProductRepository {
|
||||||
|
Future<List<ProductEntity>> getProducts(String userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
class ProductRepositoryImpl implements ProductRepository {
|
||||||
|
final ApiService apiService;
|
||||||
|
|
||||||
|
ProductRepositoryImpl(this.apiService);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<ProductEntity>> getProducts(String userId) async {
|
||||||
|
try {
|
||||||
|
final url = '${ApiEndpoints.product}$userId';
|
||||||
|
|
||||||
|
/// ✅ DEBUG PRINTS
|
||||||
|
debugPrint("🌍 PRODUCT API HIT");
|
||||||
|
debugPrint("➡️ URL: $url");
|
||||||
|
|
||||||
|
final response = await apiService.get(url);
|
||||||
|
|
||||||
|
debugPrint("✅ RAW PRODUCT API RESPONSE:");
|
||||||
|
debugPrint(response.data.toString());
|
||||||
|
|
||||||
|
/// ✅ SAFETY: If backend returns null → return empty list
|
||||||
|
if (response.data == null) {
|
||||||
|
debugPrint("⚠️ PRODUCT API RETURNED NULL → using empty list");
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ✅ SAFETY: Ensure response is really a List
|
||||||
|
if (response.data is! List) {
|
||||||
|
debugPrint("⚠️ PRODUCT API RETURNED NON-LIST DATA");
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
final List data = response.data as List;
|
||||||
|
|
||||||
|
debugPrint("✅ PARSED PRODUCT COUNT: ${data.length}");
|
||||||
|
|
||||||
|
/// ✅ EMPTY LIST IS PERFECTLY VALID
|
||||||
|
if (data.isEmpty) {
|
||||||
|
debugPrint("ℹ️ NO PRODUCTS FOUND (EMPTY LIST)");
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ✅ SAFE MODEL PARSING
|
||||||
|
return data
|
||||||
|
.map((e) => ProductModel.fromJson(e))
|
||||||
|
.toList();
|
||||||
|
} catch (e, st) {
|
||||||
|
debugPrint("❌ PRODUCT API REAL ERROR:");
|
||||||
|
debugPrint(e.toString());
|
||||||
|
debugPrint(st.toString());
|
||||||
|
|
||||||
|
/// ✅ ONLY THROW FOR REAL FAILURES
|
||||||
|
throw Exception("Products fetch error: $e");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,4 +1,3 @@
|
|||||||
// lib/data/sources/remote/api_service.dart
|
|
||||||
import 'package:autos/core/constants/api_endpoints.dart';
|
import 'package:autos/core/constants/api_endpoints.dart';
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
@ -15,7 +14,6 @@ class ApiService {
|
|||||||
headers: ApiEndpoints.defaultHeaders,
|
headers: ApiEndpoints.defaultHeaders,
|
||||||
),
|
),
|
||||||
) {
|
) {
|
||||||
// Add logging interceptor in debug mode
|
|
||||||
if (kDebugMode) {
|
if (kDebugMode) {
|
||||||
_dio.interceptors.add(
|
_dio.interceptors.add(
|
||||||
LogInterceptor(requestBody: true, responseBody: true),
|
LogInterceptor(requestBody: true, responseBody: true),
|
||||||
@ -23,7 +21,7 @@ class ApiService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// POST request
|
// ✅ KEEP OLD POST (so existing files DO NOT BREAK)
|
||||||
Future<Response> post(
|
Future<Response> post(
|
||||||
String endpoint,
|
String endpoint,
|
||||||
Map<String, dynamic> data, {
|
Map<String, dynamic> data, {
|
||||||
@ -32,13 +30,32 @@ class ApiService {
|
|||||||
return await _dio.post(endpoint, data: data, options: options);
|
return await _dio.post(endpoint, data: data, options: options);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// GET request with optional overrideBaseUrl and headers
|
// ✅ NEW POST (for overrideBaseUrl + headers + params)
|
||||||
Future<Response> get(
|
Future<Response> postWithOptions(
|
||||||
|
String endpoint, {
|
||||||
|
dynamic data,
|
||||||
|
Map<String, dynamic>? params,
|
||||||
|
Map<String, dynamic>? headers,
|
||||||
|
String? overrideBaseUrl,
|
||||||
|
}) async {
|
||||||
|
final String requestUrl =
|
||||||
|
overrideBaseUrl != null ? overrideBaseUrl + endpoint : endpoint;
|
||||||
|
|
||||||
|
return await _dio.post(
|
||||||
|
requestUrl,
|
||||||
|
data: data,
|
||||||
|
queryParameters: params,
|
||||||
|
options: headers != null ? Options(headers: headers) : null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ GET (unchanged)
|
||||||
|
Future<Response> get(
|
||||||
String endpoint, {
|
String endpoint, {
|
||||||
Map<String, dynamic>? params,
|
Map<String, dynamic>? params,
|
||||||
Map<String, dynamic>? headers, // ✅ NOW SUPPORTED
|
Map<String, dynamic>? headers,
|
||||||
String? overrideBaseUrl,
|
String? overrideBaseUrl,
|
||||||
}) async {
|
}) async {
|
||||||
final String requestUrl =
|
final String requestUrl =
|
||||||
overrideBaseUrl != null ? overrideBaseUrl + endpoint : endpoint;
|
overrideBaseUrl != null ? overrideBaseUrl + endpoint : endpoint;
|
||||||
|
|
||||||
@ -47,6 +64,5 @@ Future<Response> get(
|
|||||||
queryParameters: params,
|
queryParameters: params,
|
||||||
options: headers != null ? Options(headers: headers) : null,
|
options: headers != null ? Options(headers: headers) : null,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
13
lib/domain/entities/product.dart
Normal file
13
lib/domain/entities/product.dart
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
class ProductEntity {
|
||||||
|
final int id;
|
||||||
|
final String name;
|
||||||
|
final String image;
|
||||||
|
final double price;
|
||||||
|
|
||||||
|
ProductEntity({
|
||||||
|
required this.id,
|
||||||
|
required this.name,
|
||||||
|
required this.image,
|
||||||
|
required this.price,
|
||||||
|
});
|
||||||
|
}
|
||||||
@ -25,16 +25,11 @@ class BrandsNotifier extends StateNotifier<AsyncValue<List<BrandEntity>>> {
|
|||||||
Future<void> fetchBrands() async {
|
Future<void> fetchBrands() async {
|
||||||
state = const AsyncValue.loading();
|
state = const AsyncValue.loading();
|
||||||
try {
|
try {
|
||||||
// 1️⃣ Get user safely
|
|
||||||
final userAsync = ref.read(userDetailsProvider);
|
final userAsync = ref.read(userDetailsProvider);
|
||||||
final user = userAsync.value;
|
final user = userAsync.value;
|
||||||
|
|
||||||
if (user == null) throw Exception("User not logged in");
|
if (user == null) throw Exception("User not logged in");
|
||||||
|
|
||||||
// 2️⃣ Always fetch fresh token
|
|
||||||
final token = await repository.getTurn14AccessToken();
|
|
||||||
|
|
||||||
// 3️⃣ Fetch brands
|
|
||||||
final brands = await repository.getBrands();
|
final brands = await repository.getBrands();
|
||||||
|
|
||||||
state = AsyncValue.data(brands);
|
state = AsyncValue.data(brands);
|
||||||
@ -43,9 +38,30 @@ class BrandsNotifier extends StateNotifier<AsyncValue<List<BrandEntity>>> {
|
|||||||
debugPrint("Brands fetch error: $e");
|
debugPrint("Brands fetch error: $e");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// SAVE SELECTED BRANDS TO SERVER (Bulk Insert)
|
||||||
|
Future<void> saveSelectedBrands(List<BrandEntity> selectedBrands) async {
|
||||||
|
try {
|
||||||
|
final userAsync = ref.read(userDetailsProvider);
|
||||||
|
final user = userAsync.value;
|
||||||
|
|
||||||
|
if (user == null) throw Exception("User not logged in");
|
||||||
|
|
||||||
|
final response = await repository.saveSelectedBrands(
|
||||||
|
userId: user.id,
|
||||||
|
brands: selectedBrands,
|
||||||
|
);
|
||||||
|
|
||||||
|
debugPrint("✅ Save Success: ${response["message"]}");
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint("❌ Save Error: $e");
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// ✅ Brands State Provider
|
}
|
||||||
|
|
||||||
|
/// Brands State Provider
|
||||||
final brandsProvider =
|
final brandsProvider =
|
||||||
StateNotifierProvider<BrandsNotifier, AsyncValue<List<BrandEntity>>>(
|
StateNotifierProvider<BrandsNotifier, AsyncValue<List<BrandEntity>>>(
|
||||||
(ref) {
|
(ref) {
|
||||||
|
|||||||
55
lib/presentation/providers/products_provider.dart
Normal file
55
lib/presentation/providers/products_provider.dart
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
import 'package:autos/data/repositories/product_repository.dart';
|
||||||
|
import 'package:autos/data/sources/remote/api_service.dart';
|
||||||
|
import 'package:autos/domain/entities/product.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:flutter_riverpod/legacy.dart';
|
||||||
|
|
||||||
|
/// ✅ API SERVICE PROVIDER
|
||||||
|
final productApiServiceProvider =
|
||||||
|
Provider<ApiService>((ref) => ApiService());
|
||||||
|
|
||||||
|
/// ✅ REPOSITORY PROVIDER
|
||||||
|
final productRepositoryProvider = Provider<ProductRepositoryImpl>((ref) {
|
||||||
|
return ProductRepositoryImpl(ref.read(productApiServiceProvider));
|
||||||
|
});
|
||||||
|
|
||||||
|
/// ✅ PRODUCT STATE PROVIDER
|
||||||
|
final productProvider = StateNotifierProvider<ProductNotifier,
|
||||||
|
AsyncValue<List<ProductEntity>>>((ref) {
|
||||||
|
final repository = ref.read(productRepositoryProvider);
|
||||||
|
return ProductNotifier(repository);
|
||||||
|
});
|
||||||
|
|
||||||
|
/// ✅ PRODUCT NOTIFIER
|
||||||
|
class ProductNotifier
|
||||||
|
extends StateNotifier<AsyncValue<List<ProductEntity>>> {
|
||||||
|
final ProductRepositoryImpl repository;
|
||||||
|
|
||||||
|
ProductNotifier(this.repository)
|
||||||
|
: super(const AsyncValue.loading());
|
||||||
|
|
||||||
|
/// ✅ FETCH PRODUCTS (EMPTY LIST IS NOT AN ERROR!)
|
||||||
|
Future<void> getProducts(String userId) async {
|
||||||
|
try {
|
||||||
|
state = const AsyncValue.loading();
|
||||||
|
|
||||||
|
debugPrint("🚀 FETCHING PRODUCTS FOR USER ID = $userId");
|
||||||
|
|
||||||
|
final products = await repository.getProducts(userId);
|
||||||
|
|
||||||
|
debugPrint("✅ PRODUCTS RECEIVED = ${products.length}");
|
||||||
|
|
||||||
|
// ✅ EVEN IF LIST IS EMPTY → SUCCESS
|
||||||
|
state = AsyncValue.data(products);
|
||||||
|
} catch (e, st) {
|
||||||
|
debugPrint("❌ PRODUCT API REAL ERROR = $e");
|
||||||
|
state = AsyncValue.error(e, st);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ✅ CLEAR PRODUCTS SAFELY
|
||||||
|
void clear() {
|
||||||
|
state = const AsyncValue.data([]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,7 +1,6 @@
|
|||||||
import 'package:autos/core/theme/app_typography.dart';
|
import 'package:autos/core/theme/app_typography.dart';
|
||||||
import 'package:autos/core/widgets/hamburger_button.dart';
|
import 'package:autos/core/widgets/hamburger_button.dart';
|
||||||
import 'package:autos/core/widgets/side_menu.dart';
|
import 'package:autos/core/widgets/side_menu.dart';
|
||||||
import 'package:autos/domain/entities/brands.dart';
|
|
||||||
import 'package:autos/presentation/providers/brand_provider.dart';
|
import 'package:autos/presentation/providers/brand_provider.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
@ -15,13 +14,19 @@ class BrandsScreen extends ConsumerStatefulWidget {
|
|||||||
|
|
||||||
class _BrandsScreenState extends ConsumerState<BrandsScreen> {
|
class _BrandsScreenState extends ConsumerState<BrandsScreen> {
|
||||||
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
|
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
|
||||||
|
|
||||||
String selected = 'brands';
|
String selected = 'brands';
|
||||||
|
String searchQuery = '';
|
||||||
|
bool dropshipOnly = false;
|
||||||
|
|
||||||
|
/// ✅ NEW: Selected Brand IDs
|
||||||
|
final Set<String> selectedBrandIds = {};
|
||||||
|
|
||||||
|
bool isSaving = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
|
||||||
/// Fetch brands every time page opens
|
|
||||||
Future.microtask(() {
|
Future.microtask(() {
|
||||||
ref.read(brandsProvider.notifier).fetchBrands();
|
ref.read(brandsProvider.notifier).fetchBrands();
|
||||||
});
|
});
|
||||||
@ -40,34 +45,186 @@ class _BrandsScreenState extends ConsumerState<BrandsScreen> {
|
|||||||
),
|
),
|
||||||
body: Stack(
|
body: Stack(
|
||||||
children: [
|
children: [
|
||||||
|
/// ✅ TITLE (UNCHANGED)
|
||||||
Positioned(
|
Positioned(
|
||||||
top: topPadding,
|
top: topPadding,
|
||||||
left: 0,
|
left: 0,
|
||||||
right: 0,
|
right: 0,
|
||||||
child: Center(
|
child: Center(
|
||||||
child: Text(
|
child: Text(
|
||||||
"Brands",
|
"Turn14 Brands",
|
||||||
style: AppTypo.h2.copyWith(fontWeight: FontWeight.w700),
|
style: AppTypo.h2.copyWith(fontWeight: FontWeight.w700),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
|
/// ✅ HAMBURGER (UNCHANGED)
|
||||||
HamburgerButton(scaffoldKey: _scaffoldKey),
|
HamburgerButton(scaffoldKey: _scaffoldKey),
|
||||||
|
|
||||||
Positioned.fill(
|
Positioned.fill(
|
||||||
top: topPadding + 60,
|
top: topPadding + 50,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
/// ✅ SEARCH (UNCHANGED)
|
||||||
|
TextField(
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() => searchQuery = value.toLowerCase());
|
||||||
|
},
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: "Search brands...",
|
||||||
|
prefixIcon: const Icon(Icons.search),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
|
||||||
|
/// ✅ FILTER + SELECT ALL (UI SAME)
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
const Text("Dropship Only"),
|
||||||
|
Switch(
|
||||||
|
value: dropshipOnly,
|
||||||
|
onChanged: (val) {
|
||||||
|
setState(() => dropshipOnly = val);
|
||||||
|
},
|
||||||
|
activeColor: Colors.green,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
/// ✅ SELECT ALL (LOGIC ONLY)
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
const Text("Select All"),
|
||||||
|
Checkbox(
|
||||||
|
value: brandsState.maybeWhen(
|
||||||
|
data: (brands) =>
|
||||||
|
brands.isNotEmpty &&
|
||||||
|
selectedBrandIds.length == brands.length,
|
||||||
|
orElse: () => false,
|
||||||
|
),
|
||||||
|
onChanged: (val) {
|
||||||
|
brandsState.whenData((brands) {
|
||||||
|
setState(() {
|
||||||
|
if (val == true) {
|
||||||
|
selectedBrandIds.addAll(
|
||||||
|
brands.map((e) => e.id),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
selectedBrandIds.clear();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
/// ✅ SAVE BUTTON (NEW FEATURE ONLY)
|
||||||
|
if (selectedBrandIds.isNotEmpty)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 8),
|
||||||
|
child: ElevatedButton(
|
||||||
|
onPressed: isSaving
|
||||||
|
? null
|
||||||
|
: () async {
|
||||||
|
try {
|
||||||
|
setState(() => isSaving = true);
|
||||||
|
|
||||||
|
final allBrands = brandsState.value!;
|
||||||
|
final selectedBrands = allBrands
|
||||||
|
.where(
|
||||||
|
(b) => selectedBrandIds.contains(b.id),
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
await ref
|
||||||
|
.read(brandsProvider.notifier)
|
||||||
|
.saveSelectedBrands(selectedBrands);
|
||||||
|
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text(
|
||||||
|
"✅ Collections Saved Successfully",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
selectedBrandIds.clear();
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text("❌ Save Failed: $e"),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
setState(() => isSaving = false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: isSaving
|
||||||
|
? const CircularProgressIndicator()
|
||||||
|
: Text(
|
||||||
|
"Save Collections (${selectedBrandIds.length})",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
/// ✅ GRID (UI UNCHANGED)
|
||||||
|
Expanded(
|
||||||
child: brandsState.when(
|
child: brandsState.when(
|
||||||
loading: () => const Center(child: CircularProgressIndicator()),
|
loading: () =>
|
||||||
error: (err, st) => Center(
|
const Center(child: CircularProgressIndicator()),
|
||||||
|
error: (err, _) => Center(
|
||||||
child: Text(
|
child: Text(
|
||||||
err.toString(),
|
(() {
|
||||||
|
final raw = err.toString();
|
||||||
|
|
||||||
|
// Match 'message: ...' from string
|
||||||
|
final match = RegExp(
|
||||||
|
r'message[: ]+([^}]+)',
|
||||||
|
).firstMatch(raw);
|
||||||
|
if (match != null) {
|
||||||
|
return match
|
||||||
|
.group(1)!
|
||||||
|
.trim(); // ✅ just the message
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: remove 'Exception: ' prefix
|
||||||
|
return raw.replaceFirst(
|
||||||
|
RegExp(r'^Exception:\s*'),
|
||||||
|
'',
|
||||||
|
);
|
||||||
|
})(),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
style: const TextStyle(color: Colors.red),
|
style: const TextStyle(color: Colors.red),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
data: (brands) {
|
data: (brands) {
|
||||||
if (brands.isEmpty) {
|
final filteredBrands = brands.where((brand) {
|
||||||
return const Center(child: Text("No brands available."));
|
final nameMatch = brand.name.toLowerCase().contains(
|
||||||
}
|
searchQuery,
|
||||||
|
);
|
||||||
|
final idMatch = brand.id.toString().contains(
|
||||||
|
searchQuery,
|
||||||
|
);
|
||||||
|
final dropshipMatch = dropshipOnly
|
||||||
|
? brand.dropship
|
||||||
|
: true;
|
||||||
|
return (nameMatch || idMatch) && dropshipMatch;
|
||||||
|
}).toList();
|
||||||
|
|
||||||
return GridView.builder(
|
return GridView.builder(
|
||||||
padding: const EdgeInsets.only(bottom: 20),
|
padding: const EdgeInsets.only(bottom: 20),
|
||||||
@ -76,13 +233,16 @@ class _BrandsScreenState extends ConsumerState<BrandsScreen> {
|
|||||||
crossAxisCount: 2,
|
crossAxisCount: 2,
|
||||||
crossAxisSpacing: 12,
|
crossAxisSpacing: 12,
|
||||||
mainAxisSpacing: 12,
|
mainAxisSpacing: 12,
|
||||||
childAspectRatio: 1,
|
childAspectRatio: 0.95,
|
||||||
),
|
),
|
||||||
itemCount: brands.length,
|
itemCount: filteredBrands.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final BrandEntity brand = brands[index];
|
final brand = filteredBrands[index];
|
||||||
|
final isSelected = selectedBrandIds.contains(
|
||||||
|
brand.id,
|
||||||
|
);
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
padding: const EdgeInsets.all(12),
|
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
@ -94,29 +254,99 @@ class _BrandsScreenState extends ConsumerState<BrandsScreen> {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Stack(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
children: [
|
||||||
ClipRRect(
|
Padding(
|
||||||
borderRadius: BorderRadius.circular(8),
|
padding: const EdgeInsets.all(12),
|
||||||
|
child: Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
height: 100,
|
||||||
|
width: 100,
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius:
|
||||||
|
BorderRadius.circular(8),
|
||||||
child: Image.network(
|
child: Image.network(
|
||||||
brand.logo,
|
brand.logo,
|
||||||
height: 60,
|
fit: BoxFit.contain,
|
||||||
width: 60,
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
errorBuilder: (_, __, ___) =>
|
errorBuilder: (_, __, ___) =>
|
||||||
const Icon(Icons.image, size: 40),
|
const Icon(Icons.image),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
Text(
|
Text("ID: ${brand.id}"),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Flexible(
|
||||||
|
child: Text(
|
||||||
brand.name,
|
brand.name,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: AppTypo.h3.copyWith(
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
),
|
|
||||||
maxLines: 2,
|
maxLines: 2,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
/// ✅ DROPSHIP TAG (UNCHANGED)
|
||||||
|
if (brand.dropship)
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.topLeft,
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 8,
|
||||||
|
vertical: 4,
|
||||||
|
),
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
color: Colors.green,
|
||||||
|
borderRadius: BorderRadius.only(
|
||||||
|
topLeft: Radius.circular(12),
|
||||||
|
bottomRight: Radius.circular(12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: const Text(
|
||||||
|
'Dropship',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 12,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
/// ✅ CHECKBOX ICON (UNCHANGED POSITION)
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.topRight,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
setState(() {
|
||||||
|
if (isSelected) {
|
||||||
|
selectedBrandIds.remove(brand.id);
|
||||||
|
} else {
|
||||||
|
selectedBrandIds.add(brand.id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
child: Icon(
|
||||||
|
isSelected
|
||||||
|
? Icons.check_box
|
||||||
|
: Icons.square_outlined,
|
||||||
|
color: isSelected
|
||||||
|
? Colors.green
|
||||||
|
: Colors.grey,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@ -126,6 +356,9 @@ class _BrandsScreenState extends ConsumerState<BrandsScreen> {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
286
lib/presentation/screens/products/products_screen.dart
Normal file
286
lib/presentation/screens/products/products_screen.dart
Normal file
@ -0,0 +1,286 @@
|
|||||||
|
import 'package:autos/core/theme/app_typography.dart';
|
||||||
|
import 'package:autos/core/widgets/hamburger_button.dart';
|
||||||
|
import 'package:autos/core/widgets/side_menu.dart';
|
||||||
|
import 'package:autos/presentation/providers/products_provider.dart';
|
||||||
|
import 'package:autos/presentation/providers/user_provider.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
|
class ProductsScreen extends ConsumerStatefulWidget {
|
||||||
|
const ProductsScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
ConsumerState<ProductsScreen> createState() => _ProductsScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ProductsScreenState extends ConsumerState<ProductsScreen> {
|
||||||
|
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
|
||||||
|
String selected = 'products';
|
||||||
|
|
||||||
|
bool _hasFetched = false; // ✅ prevents multiple API calls
|
||||||
|
|
||||||
|
// Filter state
|
||||||
|
String _searchText = '';
|
||||||
|
bool _inStockOnly = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
|
||||||
|
/// ✅ Wait for widget + provider to be fully ready
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
_loadUserFromProvider();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ✅ LOAD USER FROM RIVERPOD (NOT SECURE STORAGE)
|
||||||
|
void _loadUserFromProvider() {
|
||||||
|
final userState = ref.read(userProvider);
|
||||||
|
final user = userState.value;
|
||||||
|
|
||||||
|
if (user == null || user.id == null || user.id!.isEmpty) {
|
||||||
|
debugPrint("❌ USER NOT FOUND IN PROVIDER");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_hasFetched) return; // ✅ prevent duplicate API calls
|
||||||
|
_hasFetched = true;
|
||||||
|
|
||||||
|
debugPrint(" USER FROM PROVIDER ID: ${user.id}");
|
||||||
|
|
||||||
|
ref.read(productProvider.notifier).getProducts(user.id!);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Filter products based on search text and stock
|
||||||
|
List productsFilter(List products) {
|
||||||
|
return products.where((product) {
|
||||||
|
final matchesSearch = _searchText.isEmpty ||
|
||||||
|
product.name.toLowerCase().contains(_searchText.toLowerCase());
|
||||||
|
final matchesStock = !_inStockOnly || (product.inStock ?? true);
|
||||||
|
return matchesSearch && matchesStock;
|
||||||
|
}).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final productsState = ref.watch(productProvider);
|
||||||
|
final double topPadding = MediaQuery.of(context).padding.top + 16;
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
key: _scaffoldKey,
|
||||||
|
drawer: SideMenu(
|
||||||
|
selected: selected,
|
||||||
|
onItemSelected: (key) {
|
||||||
|
setState(() => selected = key);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
body: Stack(
|
||||||
|
children: [
|
||||||
|
/// ✅ Page Title
|
||||||
|
Positioned(
|
||||||
|
top: topPadding,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
"Brand Products",
|
||||||
|
style: AppTypo.h2.copyWith(fontWeight: FontWeight.w700),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
/// ✅ Main Content
|
||||||
|
SingleChildScrollView(
|
||||||
|
physics: const BouncingScrollPhysics(),
|
||||||
|
padding: EdgeInsets.fromLTRB(16, topPadding + 55, 16, 20),
|
||||||
|
child: productsState.when(
|
||||||
|
/// ✅ LOADING
|
||||||
|
loading: () => const Center(
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.only(top: 120),
|
||||||
|
child: CircularProgressIndicator(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
/// ✅ ERROR
|
||||||
|
error: (e, _) => Center(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 120),
|
||||||
|
child: Text(
|
||||||
|
(() {
|
||||||
|
final raw = e.toString();
|
||||||
|
// Extract backend message if exists
|
||||||
|
final match = RegExp(r'message[: ]+([^}]+)').firstMatch(raw);
|
||||||
|
if (match != null) return match.group(1)!.trim();
|
||||||
|
return raw.replaceFirst(RegExp(r'^Exception:\s*'), '');
|
||||||
|
})(),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: const TextStyle(color: Colors.red),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
/// ✅ SUCCESS
|
||||||
|
data: (products) {
|
||||||
|
if (products.isEmpty) {
|
||||||
|
return const Center(
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.only(top: 120),
|
||||||
|
child: Text("No products found"),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply search / stock filter
|
||||||
|
final filteredProducts = productsFilter(products);
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
/// ✅ Search + Filter + Count Row
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
// Search bar
|
||||||
|
Expanded(
|
||||||
|
child: TextField(
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() => _searchText = value);
|
||||||
|
},
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: "Search products",
|
||||||
|
prefixIcon: const Icon(Icons.search),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(vertical: 0, horizontal: 16),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
borderSide: const BorderSide(color: Colors.grey),
|
||||||
|
),
|
||||||
|
filled: true,
|
||||||
|
fillColor: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
|
||||||
|
// In Stock Only button
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () {
|
||||||
|
setState(() => _inStockOnly = !_inStockOnly);
|
||||||
|
},
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: _inStockOnly ? Colors.green : Colors.grey,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
"In Stock",
|
||||||
|
style: const TextStyle(fontSize: 12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
|
||||||
|
// Count button
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () {},
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: Colors.deepPurple,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
"${filteredProducts.length} products",
|
||||||
|
style: const TextStyle(fontSize: 12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
/// ✅ Products Grid
|
||||||
|
GridView.builder(
|
||||||
|
shrinkWrap: true,
|
||||||
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
|
itemCount: filteredProducts.length,
|
||||||
|
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||||
|
crossAxisCount: 2,
|
||||||
|
crossAxisSpacing: 12,
|
||||||
|
mainAxisSpacing: 12,
|
||||||
|
childAspectRatio: 0.68,
|
||||||
|
),
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final product = filteredProducts[index];
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(14),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
blurRadius: 10,
|
||||||
|
color: Colors.black.withOpacity(0.05),
|
||||||
|
offset: const Offset(0, 4),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
/// ✅ Product Image
|
||||||
|
Expanded(
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
child: Image.network(
|
||||||
|
product.image,
|
||||||
|
width: double.infinity,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
errorBuilder: (_, __, ___) =>
|
||||||
|
const Icon(Icons.image_not_supported),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
|
||||||
|
/// ✅ Product Name
|
||||||
|
Text(
|
||||||
|
product.name,
|
||||||
|
maxLines: 2,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
|
||||||
|
/// ✅ Price
|
||||||
|
Text(
|
||||||
|
"₹ ${product.price}",
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.green,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
/// ✅ Hamburger Button
|
||||||
|
HamburgerButton(scaffoldKey: _scaffoldKey),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user