BackendCall Implemented for GET Token and Brands data.
This commit is contained in:
parent
41970033ad
commit
595554d541
@ -1,5 +1,5 @@
|
||||
class ApiEndpoints {
|
||||
static const String baseUrl = 'https://ebay.backend.data4autos.com/api';
|
||||
static const String baseUrl = 'https://ebay.backend.data4autos.com';
|
||||
|
||||
static const Duration connectTimeout = Duration(seconds: 10);
|
||||
static const Duration receiveTimeout = Duration(seconds: 10);
|
||||
@ -9,12 +9,18 @@ class ApiEndpoints {
|
||||
};
|
||||
|
||||
///Authentication
|
||||
static const login = '/auth/login';
|
||||
static const signup = '/auth/signup';
|
||||
static const forgotPassword = '/auth/forgot-password';
|
||||
static const userDetails = '/auth/users/';
|
||||
static const login = '/api/auth/login';
|
||||
static const signup = '/api/auth/signup';
|
||||
static const forgotPassword = '/api/auth/forgot-password';
|
||||
static const userDetails = '/api/auth/users/';
|
||||
|
||||
///GET Token
|
||||
static const getToken = '/api/auth/turn14/get-access-token';
|
||||
|
||||
///Turn14
|
||||
static const turn14Save = '/auth/turn14/save';
|
||||
static const turn14Status = '/auth/turn14/status';
|
||||
static const turn14Save = '/api/auth/turn14/save';
|
||||
static const turn14Status = '/api/auth/turn14/status';
|
||||
|
||||
///Brands
|
||||
static const String brands = "/v1/brands";
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@ import 'package:autos/core/routing/route_paths.dart';
|
||||
import 'package:autos/presentation/screens/auth/forgot_password_screen.dart';
|
||||
import 'package:autos/presentation/screens/auth/login_screen.dart';
|
||||
import 'package:autos/presentation/screens/auth/sign_up_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/ebay/ebay_screen.dart';
|
||||
import 'package:autos/presentation/screens/store/create_location_screen.dart';
|
||||
@ -57,6 +58,9 @@ class AppRouter {
|
||||
case AppRoutePaths.createStoreLocation:
|
||||
return slideRoute(CreateLocationScreen());
|
||||
|
||||
case AppRoutePaths.brands:
|
||||
return slideRoute(BrandsScreen());
|
||||
|
||||
default:
|
||||
return _defaultFallback(settings);
|
||||
}
|
||||
|
||||
@ -7,4 +7,5 @@ class AppRoutePaths {
|
||||
static const ebay = '/ebay';
|
||||
static const store = '/store';
|
||||
static const createStoreLocation = '/createStoreLocation';
|
||||
static const brands = '/brands';
|
||||
}
|
||||
|
||||
@ -64,7 +64,7 @@ class SideMenu extends ConsumerWidget {
|
||||
|
||||
// --- MANAGE ---
|
||||
_sectionHeader("MANAGE"),
|
||||
_menuItem(context, "🏷️", "Brands", 'brands'),
|
||||
_menuItem(context, "🏷️", "Brands", AppRoutePaths.brands),
|
||||
_menuItem(context, "📦", "Products", 'products'),
|
||||
_menuItem(context, "⬇️", "Imports", 'imports'),
|
||||
|
||||
|
||||
107
lib/data/models/brands_model.dart
Normal file
107
lib/data/models/brands_model.dart
Normal file
@ -0,0 +1,107 @@
|
||||
import 'package:autos/domain/entities/brands.dart';
|
||||
|
||||
class Brand {
|
||||
final String id;
|
||||
final String name;
|
||||
final String logo;
|
||||
final bool dropship;
|
||||
final List<PriceGroup> pricegroups;
|
||||
|
||||
Brand({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.logo,
|
||||
required this.dropship,
|
||||
required this.pricegroups,
|
||||
});
|
||||
|
||||
factory Brand.fromJson(Map<String, dynamic> json) {
|
||||
return Brand(
|
||||
id: json['id'].toString(),
|
||||
name: json['name'] ?? '',
|
||||
logo: json['logo'] ?? '',
|
||||
dropship: json['dropship'] ?? false,
|
||||
pricegroups: (json['pricegroups'] as List<dynamic>?)
|
||||
?.map((e) => PriceGroup.fromJson(e))
|
||||
.toList() ??
|
||||
[],
|
||||
);
|
||||
}
|
||||
|
||||
/// 🔥 Model → Entity
|
||||
BrandEntity toEntity() {
|
||||
return BrandEntity(
|
||||
id: id,
|
||||
name: name,
|
||||
logo: logo,
|
||||
dropship: dropship,
|
||||
pricegroups: pricegroups.map((e) => e.toEntity()).toList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class PriceGroup {
|
||||
final String pricegroupId;
|
||||
final String pricegroupName;
|
||||
final String pricegroupPrefix;
|
||||
final List<dynamic> locationRules;
|
||||
final List<PurchaseRestriction> purchaseRestrictions;
|
||||
|
||||
PriceGroup({
|
||||
required this.pricegroupId,
|
||||
required this.pricegroupName,
|
||||
required this.pricegroupPrefix,
|
||||
required this.locationRules,
|
||||
required this.purchaseRestrictions,
|
||||
});
|
||||
|
||||
factory PriceGroup.fromJson(Map<String, dynamic> json) {
|
||||
return PriceGroup(
|
||||
pricegroupId: json['pricegroup_id'].toString(),
|
||||
pricegroupName: json['pricegroup_name'] ?? '',
|
||||
pricegroupPrefix: json['pricegroup_prefix'] ?? '',
|
||||
locationRules: json['location_rules'] ?? [],
|
||||
purchaseRestrictions: (json['purchase_restrictions'] as List<dynamic>?)
|
||||
?.map((e) => PurchaseRestriction.fromJson(e))
|
||||
.toList() ??
|
||||
[],
|
||||
);
|
||||
}
|
||||
|
||||
/// 🔥 Model → Entity
|
||||
PriceGroupEntity toEntity() {
|
||||
return PriceGroupEntity(
|
||||
pricegroupId: pricegroupId,
|
||||
pricegroupName: pricegroupName,
|
||||
pricegroupPrefix: pricegroupPrefix,
|
||||
locationRules: locationRules,
|
||||
purchaseRestrictions:
|
||||
purchaseRestrictions.map((e) => e.toEntity()).toList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class PurchaseRestriction {
|
||||
final String program;
|
||||
final String yourStatus;
|
||||
|
||||
PurchaseRestriction({
|
||||
required this.program,
|
||||
required this.yourStatus,
|
||||
});
|
||||
|
||||
factory PurchaseRestriction.fromJson(Map<String, dynamic> json) {
|
||||
return PurchaseRestriction(
|
||||
program: json['program'] ?? '',
|
||||
yourStatus: json['your_status'] ?? '',
|
||||
);
|
||||
}
|
||||
|
||||
/// 🔥 Model → Entity
|
||||
PurchaseRestrictionEntity toEntity() {
|
||||
return PurchaseRestrictionEntity(
|
||||
program: program,
|
||||
yourStatus: yourStatus,
|
||||
);
|
||||
}
|
||||
}
|
||||
91
lib/data/repositories/brands_repository_impl.dart
Normal file
91
lib/data/repositories/brands_repository_impl.dart
Normal file
@ -0,0 +1,91 @@
|
||||
import 'dart:convert';
|
||||
import 'package:autos/core/constants/api_endpoints.dart';
|
||||
import 'package:autos/data/models/brands_model.dart';
|
||||
import 'package:autos/data/sources/remote/api_service.dart';
|
||||
import 'package:autos/domain/entities/brands.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:autos/data/models/user_model.dart';
|
||||
|
||||
abstract class BrandsRepository {
|
||||
Future<List<BrandEntity>> getBrands();
|
||||
}
|
||||
|
||||
class BrandsRepositoryImpl implements BrandsRepository {
|
||||
final ApiService _apiService;
|
||||
final _storage = const FlutterSecureStorage();
|
||||
static const _userKey = 'logged_in_user';
|
||||
|
||||
BrandsRepositoryImpl(this._apiService);
|
||||
|
||||
/// ✅ ALWAYS CALL TOKEN API (FORCE REFRESH)
|
||||
Future<String> getTurn14AccessToken() async {
|
||||
final jsonString = await _storage.read(key: _userKey);
|
||||
if (jsonString == null) {
|
||||
throw Exception("User not found. Please login again.");
|
||||
}
|
||||
|
||||
final user = UserModel.fromJson(jsonDecode(jsonString));
|
||||
|
||||
final response = await _apiService.post(ApiEndpoints.getToken, {
|
||||
"userid": user.id,
|
||||
});
|
||||
|
||||
final data = response.data;
|
||||
|
||||
String token;
|
||||
|
||||
if (data["code"] == "TOKEN_UPDATED") {
|
||||
// ✅ New token returned
|
||||
token = data["access_token"];
|
||||
await _storage.write(key: 'turn14_token', value: token);
|
||||
} else if (data["code"] == "TOKEN_VALID") {
|
||||
// ✅ Token still valid, read from storage
|
||||
token = await _storage.read(key: 'turn14_token') ?? '';
|
||||
if (token.isEmpty) {
|
||||
throw Exception("Token missing in storage");
|
||||
}
|
||||
} else {
|
||||
throw Exception(data["message"]);
|
||||
}
|
||||
|
||||
debugPrint("Turn14 Token: $token");
|
||||
return token;
|
||||
}
|
||||
|
||||
/// ✅ FETCH BRANDS USING FRESH TOKEN EVERY TIME
|
||||
@override
|
||||
Future<List<BrandEntity>> getBrands() async {
|
||||
try {
|
||||
final token = await getTurn14AccessToken();
|
||||
|
||||
final response = await _apiService.get(
|
||||
'/v1/brands',
|
||||
overrideBaseUrl: 'https://turn14.data4autos.com',
|
||||
headers: {
|
||||
'Authorization': 'Bearer $token',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
);
|
||||
|
||||
if (response.statusCode != 200) {
|
||||
throw Exception("Server error: ${response.statusCode}");
|
||||
}
|
||||
|
||||
final data = response.data;
|
||||
|
||||
if (data is! Map || data["data"] == null) {
|
||||
throw Exception("Invalid API response");
|
||||
}
|
||||
|
||||
final list = data["data"] as List;
|
||||
|
||||
return list.map((json) => Brand.fromJson(json).toEntity()).toList();
|
||||
} on DioException catch (e) {
|
||||
throw Exception("Network error: ${e.response?.data ?? e.message}");
|
||||
} catch (e) {
|
||||
throw Exception("Unexpected error: $e");
|
||||
}
|
||||
}
|
||||
}
|
||||
57
lib/data/repositories/token_repository.dart
Normal file
57
lib/data/repositories/token_repository.dart
Normal file
@ -0,0 +1,57 @@
|
||||
import 'package:autos/core/constants/api_endpoints.dart';
|
||||
import 'package:autos/data/sources/remote/api_service.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
|
||||
class TokenRepository {
|
||||
final ApiService apiService;
|
||||
final _storage = const FlutterSecureStorage();
|
||||
|
||||
TokenRepository(this.apiService);
|
||||
|
||||
static const _tokenKey = "turn14_token";
|
||||
static const _tokenTimeKey = "turn14_token_time";
|
||||
|
||||
/// ✅ Get Valid Token (Auto Refresh If Expired)
|
||||
Future<String> getValidToken(String userId) async {
|
||||
final token = await _storage.read(key: _tokenKey);
|
||||
final time = await _storage.read(key: _tokenTimeKey);
|
||||
|
||||
if (token != null && time != null) {
|
||||
final savedTime = DateTime.parse(time);
|
||||
final diff = DateTime.now().difference(savedTime);
|
||||
|
||||
/// ✅ Token Valid for 50 minutes
|
||||
if (diff.inMinutes < 50) {
|
||||
return token;
|
||||
}
|
||||
}
|
||||
|
||||
/// 🔄 Token Expired → Refresh
|
||||
return await _refreshToken(userId);
|
||||
}
|
||||
|
||||
/// 🔄 Refresh Token API
|
||||
Future<String> _refreshToken(String userId) async {
|
||||
final response = await apiService.post(
|
||||
ApiEndpoints.getToken,
|
||||
{"userid": userId},
|
||||
);
|
||||
|
||||
final data = response.data;
|
||||
|
||||
if (data["code"] != "TOKEN_UPDATED") {
|
||||
throw Exception(data["message"]);
|
||||
}
|
||||
|
||||
final token = data["access_token"];
|
||||
|
||||
/// ✅ Save New Token + Timestamp
|
||||
await _storage.write(key: _tokenKey, value: token);
|
||||
await _storage.write(
|
||||
key: _tokenTimeKey,
|
||||
value: DateTime.now().toIso8601String(),
|
||||
);
|
||||
|
||||
return token;
|
||||
}
|
||||
}
|
||||
@ -1,3 +1,4 @@
|
||||
// lib/data/sources/remote/api_service.dart
|
||||
import 'package:autos/core/constants/api_endpoints.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
@ -6,14 +7,14 @@ class ApiService {
|
||||
final Dio _dio;
|
||||
|
||||
ApiService({String? baseUrl})
|
||||
: _dio = Dio(
|
||||
BaseOptions(
|
||||
baseUrl: baseUrl ?? ApiEndpoints.baseUrl,
|
||||
connectTimeout: ApiEndpoints.connectTimeout,
|
||||
receiveTimeout: ApiEndpoints.receiveTimeout,
|
||||
headers: ApiEndpoints.defaultHeaders,
|
||||
),
|
||||
) {
|
||||
: _dio = Dio(
|
||||
BaseOptions(
|
||||
baseUrl: baseUrl ?? ApiEndpoints.baseUrl,
|
||||
connectTimeout: ApiEndpoints.connectTimeout,
|
||||
receiveTimeout: ApiEndpoints.receiveTimeout,
|
||||
headers: ApiEndpoints.defaultHeaders,
|
||||
),
|
||||
) {
|
||||
// Add logging interceptor in debug mode
|
||||
if (kDebugMode) {
|
||||
_dio.interceptors.add(
|
||||
@ -22,6 +23,7 @@ class ApiService {
|
||||
}
|
||||
}
|
||||
|
||||
/// POST request
|
||||
Future<Response> post(
|
||||
String endpoint,
|
||||
Map<String, dynamic> data, {
|
||||
@ -30,7 +32,21 @@ class ApiService {
|
||||
return await _dio.post(endpoint, data: data, options: options);
|
||||
}
|
||||
|
||||
Future<Response> get(String endpoint, {Map<String, dynamic>? params}) async {
|
||||
return await _dio.get(endpoint, queryParameters: params);
|
||||
}
|
||||
/// GET request with optional overrideBaseUrl and headers
|
||||
Future<Response> get(
|
||||
String endpoint, {
|
||||
Map<String, dynamic>? params,
|
||||
Map<String, dynamic>? headers, // ✅ NOW SUPPORTED
|
||||
String? overrideBaseUrl,
|
||||
}) async {
|
||||
final String requestUrl =
|
||||
overrideBaseUrl != null ? overrideBaseUrl + endpoint : endpoint;
|
||||
|
||||
return await _dio.get(
|
||||
requestUrl,
|
||||
queryParameters: params,
|
||||
options: headers != null ? Options(headers: headers) : null,
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
41
lib/domain/entities/brands.dart
Normal file
41
lib/domain/entities/brands.dart
Normal file
@ -0,0 +1,41 @@
|
||||
class BrandEntity {
|
||||
final String id;
|
||||
final String name;
|
||||
final String logo;
|
||||
final bool dropship;
|
||||
final List<PriceGroupEntity> pricegroups;
|
||||
|
||||
BrandEntity({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.logo,
|
||||
required this.dropship,
|
||||
required this.pricegroups,
|
||||
});
|
||||
}
|
||||
|
||||
class PriceGroupEntity {
|
||||
final String pricegroupId;
|
||||
final String pricegroupName;
|
||||
final String pricegroupPrefix;
|
||||
final List<dynamic> locationRules;
|
||||
final List<PurchaseRestrictionEntity> purchaseRestrictions;
|
||||
|
||||
PriceGroupEntity({
|
||||
required this.pricegroupId,
|
||||
required this.pricegroupName,
|
||||
required this.pricegroupPrefix,
|
||||
required this.locationRules,
|
||||
required this.purchaseRestrictions,
|
||||
});
|
||||
}
|
||||
|
||||
class PurchaseRestrictionEntity {
|
||||
final String program;
|
||||
final String yourStatus;
|
||||
|
||||
PurchaseRestrictionEntity({
|
||||
required this.program,
|
||||
required this.yourStatus,
|
||||
});
|
||||
}
|
||||
55
lib/presentation/providers/brand_provider.dart
Normal file
55
lib/presentation/providers/brand_provider.dart
Normal file
@ -0,0 +1,55 @@
|
||||
import 'package:autos/data/repositories/brands_repository_impl.dart';
|
||||
import 'package:autos/data/sources/remote/api_service.dart';
|
||||
import 'package:autos/domain/entities/brands.dart';
|
||||
import 'package:autos/presentation/providers/user_provider.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_riverpod/legacy.dart';
|
||||
|
||||
/// ✅ API Service Provider
|
||||
final brandsApiServiceProvider = Provider<ApiService>((ref) => ApiService());
|
||||
|
||||
/// ✅ Repository Provider
|
||||
final brandsRepositoryProvider = Provider<BrandsRepositoryImpl>(
|
||||
(ref) => BrandsRepositoryImpl(ref.read(brandsApiServiceProvider)),
|
||||
);
|
||||
|
||||
/// ✅ Brands Notifier
|
||||
class BrandsNotifier extends StateNotifier<AsyncValue<List<BrandEntity>>> {
|
||||
final BrandsRepositoryImpl repository;
|
||||
final Ref ref;
|
||||
|
||||
BrandsNotifier(this.repository, this.ref) : super(const AsyncValue.loading());
|
||||
|
||||
/// Fetch brands every time page opens
|
||||
Future<void> fetchBrands() async {
|
||||
state = const AsyncValue.loading();
|
||||
try {
|
||||
// 1️⃣ Get user safely
|
||||
final userAsync = ref.read(userDetailsProvider);
|
||||
final user = userAsync.value;
|
||||
|
||||
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();
|
||||
|
||||
state = AsyncValue.data(brands);
|
||||
} catch (e, st) {
|
||||
state = AsyncValue.error(e, st);
|
||||
debugPrint("Brands fetch error: $e");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ✅ Brands State Provider
|
||||
final brandsProvider =
|
||||
StateNotifierProvider<BrandsNotifier, AsyncValue<List<BrandEntity>>>(
|
||||
(ref) {
|
||||
final repo = ref.read(brandsRepositoryProvider);
|
||||
return BrandsNotifier(repo, ref);
|
||||
},
|
||||
);
|
||||
7
lib/presentation/providers/token_provider.dart
Normal file
7
lib/presentation/providers/token_provider.dart
Normal file
@ -0,0 +1,7 @@
|
||||
import 'package:autos/presentation/providers/user_provider.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:autos/data/repositories/token_repository.dart';
|
||||
|
||||
final tokenRepositoryProvider = Provider<TokenRepository>(
|
||||
(ref) => TokenRepository(ref.read(apiServiceProvider)),
|
||||
);
|
||||
134
lib/presentation/screens/brands/brands_screen.dart
Normal file
134
lib/presentation/screens/brands/brands_screen.dart
Normal file
@ -0,0 +1,134 @@
|
||||
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/domain/entities/brands.dart';
|
||||
import 'package:autos/presentation/providers/brand_provider.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
class BrandsScreen extends ConsumerStatefulWidget {
|
||||
const BrandsScreen({super.key});
|
||||
|
||||
@override
|
||||
ConsumerState<BrandsScreen> createState() => _BrandsScreenState();
|
||||
}
|
||||
|
||||
class _BrandsScreenState extends ConsumerState<BrandsScreen> {
|
||||
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
|
||||
String selected = 'brands';
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
/// Fetch brands every time page opens
|
||||
Future.microtask(() {
|
||||
ref.read(brandsProvider.notifier).fetchBrands();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final topPadding = MediaQuery.of(context).padding.top + 16;
|
||||
final brandsState = ref.watch(brandsProvider);
|
||||
|
||||
return Scaffold(
|
||||
key: _scaffoldKey,
|
||||
drawer: SideMenu(
|
||||
selected: selected,
|
||||
onItemSelected: (key) => setState(() => selected = key),
|
||||
),
|
||||
body: Stack(
|
||||
children: [
|
||||
Positioned(
|
||||
top: topPadding,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Center(
|
||||
child: Text(
|
||||
"Brands",
|
||||
style: AppTypo.h2.copyWith(fontWeight: FontWeight.w700),
|
||||
),
|
||||
),
|
||||
),
|
||||
HamburgerButton(scaffoldKey: _scaffoldKey),
|
||||
Positioned.fill(
|
||||
top: topPadding + 60,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: brandsState.when(
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error: (err, st) => Center(
|
||||
child: Text(
|
||||
err.toString(),
|
||||
style: const TextStyle(color: Colors.red),
|
||||
),
|
||||
),
|
||||
data: (brands) {
|
||||
if (brands.isEmpty) {
|
||||
return const Center(child: Text("No brands available."));
|
||||
}
|
||||
|
||||
return GridView.builder(
|
||||
padding: const EdgeInsets.only(bottom: 20),
|
||||
gridDelegate:
|
||||
const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 2,
|
||||
crossAxisSpacing: 12,
|
||||
mainAxisSpacing: 12,
|
||||
childAspectRatio: 1,
|
||||
),
|
||||
itemCount: brands.length,
|
||||
itemBuilder: (context, index) {
|
||||
final BrandEntity brand = brands[index];
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: Colors.black12,
|
||||
blurRadius: 6,
|
||||
offset: Offset(0, 3),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Image.network(
|
||||
brand.logo,
|
||||
height: 60,
|
||||
width: 60,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (_, __, ___) =>
|
||||
const Icon(Icons.image, size: 40),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
brand.name,
|
||||
textAlign: TextAlign.center,
|
||||
style: AppTypo.h3.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user