Commit c8abf95b authored by DatHV's avatar DatHV
Browse files

update screen logic

parent fda33894
......@@ -9,13 +9,51 @@ part of 'working_site_model.dart';
WorkingSiteModel _$WorkingSiteModelFromJson(Map<String, dynamic> json) =>
WorkingSiteModel(
id: json['id'] as String?,
name: json['name'] as String?,
avatar: json['avatar'] as String?,
primaryMembership:
json['primary_membership'] == null
? null
: PrimaryMembershipModel.fromJson(
json['primary_membership'] as Map<String, dynamic>,
),
);
Map<String, dynamic> _$WorkingSiteModelToJson(WorkingSiteModel instance) =>
<String, dynamic>{
'id': instance.id,
'name': instance.name,
'avatar': instance.avatar,
'primary_membership': instance.primaryMembership,
};
PrimaryMembershipModel _$PrimaryMembershipModelFromJson(
Map<String, dynamic> json,
) => PrimaryMembershipModel(
membershipLevel:
json['membership_level'] == null
? null
: MembershipLevelShortModel.fromJson(
json['membership_level'] as Map<String, dynamic>,
),
);
Map<String, dynamic> _$PrimaryMembershipModelToJson(
PrimaryMembershipModel instance,
) => <String, dynamic>{'membership_level': instance.membershipLevel};
MembershipLevelShortModel _$MembershipLevelShortModelFromJson(
Map<String, dynamic> json,
) => MembershipLevelShortModel(
levelId: json['levelId'] as String?,
levelCode: json['level_code'] as String?,
levelName: json['level_name'] as String?,
levelLogo: json['level_logo'] as String?,
);
Map<String, dynamic> _$MembershipLevelShortModelToJson(
MembershipLevelShortModel instance,
) => <String, dynamic>{
'levelId': instance.levelId,
'level_code': instance.levelCode,
'level_name': instance.levelName,
'level_logo': instance.levelLogo,
};
......@@ -15,6 +15,24 @@ class RestfulAPIClient {
RestfulAPIClient(this._dio);
Json header = {};
Future<BaseResponseModel<T>> fetchObject<T>(
String path,
T Function(dynamic json) fromJsonT,
) async {
final response = await _dio.get(path);
return BaseResponseModel<T>.fromJson(response.data, fromJsonT);
}
Future<BaseResponseModel<List<T>>> fetchList<T>(
String path,
T Function(dynamic json) fromJson,
) async {
final res = await _dio.get(path);
return BaseResponseModel<List<T>>.fromJson(res.data, (json) {
return (json as List).map((e) => fromJson(e)).toList();
});
}
Future<BaseResponseModel<T>> requestNormal<T>(String path, Method method, Json params, CallbackReturn<T, dynamic> parser) async {
final result = await request<BaseResponseModel<T>>(path, method, params, (data) {
return BaseResponseModel<T>.fromJson(data, (json) => parser(json));
......
......@@ -18,8 +18,12 @@ import '../screen/game/models/game_bundle_item_model.dart';
import '../screen/home/models/achievement_model.dart';
import '../screen/home/models/hover_data_model.dart';
import '../screen/home/models/main_section_config_model.dart';
import '../screen/home/models/my_product_model.dart';
import '../screen/home/models/notification_unread_model.dart';
import '../screen/home/models/pipi_detail_model.dart';
import '../screen/location_address/models/district_address_model.dart';
import '../screen/location_address/models/province_address_model.dart';
import '../screen/membership/models/membership_info_response.dart';
import '../screen/notification/models/category_notify_item_model.dart';
import '../screen/notification/models/notification_detail_model.dart';
import '../screen/notification/models/notification_list_data_model.dart';
......@@ -139,7 +143,6 @@ extension RestfullAPIClientAllApi on RestfulAPIClient {
}
Future<BaseResponseModel<ProfileResponseModel>> getUserProfile() async {
var deviceKey = await DeviceInfo.getDeviceId();
return requestNormal(APIPaths.getUserInfo, Method.GET, {}, (data) => ProfileResponseModel.fromJson(data as Json));
}
......@@ -505,14 +508,7 @@ extension RestfullAPIClientAllApi on RestfulAPIClient {
});
}
Future<BaseResponseModel<AchievementListResponse>> getAchievementList() async {
String? token = DataPreference.instance.token ?? "";
final body = {
"access_token": token,
"achievement_type": "Earn_Point",
"start": 0,
"limit": 1000,
};
Future<BaseResponseModel<AchievementListResponse>> getAchievementList(Json body) async {
return requestNormal(APIPaths.achievementGetList, Method.POST, body, (data) {
return AchievementListResponse.fromJson(data as Json);
});
......@@ -525,14 +521,55 @@ extension RestfullAPIClientAllApi on RestfulAPIClient {
}
Future<BaseResponseModel<NotificationUnreadData>> getNotificationUnread() async {
return requestNormal(APIPaths.getNotificationUnread, Method.POST, {}, (data) {
String? token = DataPreference.instance.token ?? "";
final body = {"access_token": token};
return requestNormal(APIPaths.getNotificationUnread, Method.POST, body, (data) {
return NotificationUnreadData.fromJson(data as Json);
});
}
Future<BaseResponseModel<HeaderHomeModel>> getDynamicHeaderHome() async {
return requestNormal(APIPaths.getDynamicHeaderHome, Method.POST, {}, (data) {
return requestNormal(APIPaths.getDynamicHeaderHome, Method.GET, {}, (data) {
return HeaderHomeModel.fromJson(data as Json);
});
}
Future<BaseResponseModel<List<MyProductModel>>> getCustomerProducts(Json body) async {
return requestNormal(APIPaths.getCustomerProducts, Method.GET, body, (data) {
final list = data as List<dynamic>;
return list.map((e) => MyProductModel.fromJson(e)).toList();
});
}
Future<BaseResponseModel<EmptyCodable>> updateWorkerSiteProfile(Json body) async {
String? token = DataPreference.instance.token ?? "";
body["access_token"] = token;
return requestNormal(APIPaths.updateWorkerSiteProfile, Method.POST, body, (data) {
return EmptyCodable.fromJson(data as Json);
});
}
Future<BaseResponseModel<ProvinceAddressResponse>> locationProvinceGetList() async {
String? token = DataPreference.instance.token ?? "";
final body = {"access_token": token, "country_code2": "VN"};
return requestNormal(APIPaths.locationProvinceGetList, Method.POST, body, (data) {
return ProvinceAddressResponse.fromJson(data as Json);
});
}
Future<BaseResponseModel<DistrictAddressResponse>> locationDistrictGetList(String provinceCode) async {
String? token = DataPreference.instance.token ?? "";
final body = {"access_token": token, "province_code": provinceCode};
return requestNormal(APIPaths.locationDistrictGetList, Method.POST, body, (data) {
return DistrictAddressResponse.fromJson(data as Json);
});
}
Future<BaseResponseModel<MembershipInfoResponse>> getMembershipLevelInfo() async {
String? token = DataPreference.instance.token ?? "";
final body = {"access_token": token};
return requestNormal(APIPaths.getMembershipLevelInfo, Method.POST, body, (data) {
return MembershipInfoResponse.fromJson(data as Json);
});
}
}
\ No newline at end of file
......@@ -23,7 +23,8 @@ class DataPreference {
_profile = ProfileResponseModel.fromJson(jsonDecode(profileJson));
}
}
String get fullName => _profile?.workerSite?.fullname ?? "Quý Khách";
String? get rankName => _profile?.workingSite?.primaryMembership?.membershipLevel?.levelName ?? "";
String? get token => _loginToken?.accessToken;
String? get phone => _profile?.workerSite?.phoneNumber;
bool get logged => (token ?? "").isNotEmpty;
......
import 'package:package_info_plus/package_info_plus.dart';
class AppInfoHelper {
static Future<String> get version async {
final info = await PackageInfo.fromPlatform();
return info.version; // tương đương CFBundleShortVersionString
}
static Future<String> get buildNumber async {
final info = await PackageInfo.fromPlatform();
return info.buildNumber; // tương đương CFBundleVersion
}
}
......@@ -2,47 +2,115 @@ import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:mypoint_flutter_app/widgets/image_loader.dart';
import '../../widgets/custom_navigation_bar.dart';
import '../home/models/achievement_model.dart';
import 'achievement_viewmodel.dart';
class AchievementListScreen extends StatelessWidget {
class AchievementListScreen extends StatefulWidget {
const AchievementListScreen({super.key});
@override
Widget build(BuildContext context) {
final viewModel = Get.put(AchievementViewModel());
State<AchievementListScreen> createState() => _AchievementListScreenState();
}
class _AchievementListScreenState extends State<AchievementListScreen> {
late final AchievementViewModel _viewModel;
String _title = "Thử thách trúng quà";
bool isPointHunting = false;
@override
void initState() {
super.initState();
final args = Get.arguments;
if (args is bool) {
isPointHunting = args;
}
if (isPointHunting == true) {
_title = "Săn điểm";
}
_viewModel = Get.put(AchievementViewModel(isPointHunting: isPointHunting));
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: CustomNavigationBar(title: "Thử thách trúng quà"),
appBar: CustomNavigationBar(title: _title),
body: Obx(() {
final items = viewModel.achievements;
return Padding(
padding: const EdgeInsets.all(16.0),
child: GridView.builder(
itemCount: items.length,
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 12,
mainAxisSpacing: 12,
childAspectRatio: 4/3,
if (isPointHunting) {
return _buildPointHuntingContent();
} else {
return _buildAchievementContent();
}
}),
);
}
Widget _buildAchievementContent() {
final items = _viewModel.achievements;
return Padding(
padding: const EdgeInsets.all(16.0),
child: GridView.builder(
itemCount: items.length,
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 12,
mainAxisSpacing: 12,
childAspectRatio: 4 / 3,
),
itemBuilder: (context, index) {
final item = items[index];
return GestureDetector(
onTap: () {
item.directionScreen?.begin();
},
child: ClipRRect(
borderRadius: BorderRadius.circular(12),
child: loadNetworkImage(
url: item.urlBackground ?? '',
placeholderAsset: 'assets/images/bg_default_34.png',
),
),
);
},
),
);
}
Widget _buildPointHuntingContent() {
final items = _viewModel.achievements;
return RefreshIndicator(
onRefresh: () => _viewModel.fetchAchievements(),
child: CustomScrollView(
physics: const AlwaysScrollableScrollPhysics(),
slivers: [
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 12),
child: Image.asset("assets/images/bg_header_hunt_points.png"),
),
itemBuilder: (context, index) {
),
SliverList(
delegate: SliverChildBuilderDelegate((context, index) {
final item = items[index];
return GestureDetector(
onTap: () {
item.directionScreen?.begin();
},
child: ClipRRect(
borderRadius: BorderRadius.circular(12),
child: loadNetworkImage(
url: item.urlBackground ?? '',
placeholderAsset: 'assets/images/bg_default_34.png',
),
),
);
},
return _buildPointHuntingItem(item);
}, childCount: items.length),
),
);
}),
],
),
);
}
Widget _buildPointHuntingItem(AchievementModel item) {
return GestureDetector(
onTap: () {
item.directionScreen?.begin();
},
child: Padding(
padding: const EdgeInsets.only(left: 16, right: 16, top: 8, bottom: 8),
child: ClipRRect(
borderRadius: BorderRadius.circular(12),
child: loadNetworkImage(url: item.urlBackground ?? '', placeholderAsset: 'assets/images/bg_default_169.png'),
),
),
);
}
}
import 'package:get/get.dart';
import 'package:mypoint_flutter_app/networking/restful_api_request.dart';
import '../../base/restful_api_viewmodel.dart';
import '../../preference/data_preference.dart';
import '../home/models/achievement_model.dart';
class AchievementViewModel extends RestfulApiViewModel {
bool isPointHunting;
var achievements = <AchievementModel>[].obs;
AchievementViewModel({this.isPointHunting = false});
@override
void onInit() {
......@@ -13,9 +16,16 @@ class AchievementViewModel extends RestfulApiViewModel {
}
Future<void> fetchAchievements() async {
String? token = DataPreference.instance.token ?? "";
final body = {
"access_token": token,
"achievement_type": isPointHunting ? "Hunt_Point": "Earn_Point",
"start": 0,
"limit": 1000,
};
showLoading();
try {
final response = await client.getAchievementList();
final response = await client.getAchievementList(body);
if (response.data != null) {
achievements.value = response.data?.achievements ?? [];
}
......
......@@ -5,6 +5,7 @@ import '../../base/basic_state.dart';
import '../../shared/router_gage.dart';
import '../../widgets/custom_empty_widget.dart';
import '../../widgets/custom_navigation_bar.dart';
import '../home/header_home_viewmodel.dart';
import 'game_tab_viewmodel.dart';
class GameTabScreen extends BaseScreen {
......@@ -15,6 +16,7 @@ class GameTabScreen extends BaseScreen {
}
class _GameTabScreenState extends BaseState<GameTabScreen> with BasicState {
final _headerHomeVM = Get.find<HeaderHomeViewModel>();
final GameTabViewModel _viewModel = Get.put(GameTabViewModel());
final LayerLink _layerLink = LayerLink();
final GlobalKey _infoKey = GlobalKey();
......@@ -45,6 +47,7 @@ class _GameTabScreenState extends BaseState<GameTabScreen> with BasicState {
appBar: CustomNavigationBar(
title: "Games",
showBackButton: false,
backgroundImage: _headerHomeVM.headerData.background ?? "assets/images/bg_header_navi.png",
rightButtons: [
CompositedTransformTarget(
link: _layerLink,
......
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import '../../../widgets/image_loader.dart';
import '../../voucher/sub_widget/voucher_section_title.dart';
import '../models/achievement_model.dart';
import '../models/main_section_config_model.dart';
class AchievementCarousel extends StatelessWidget {
final List<AchievementModel> items;
final MainSectionConfigModel? sectionConfig;
final void Function(AchievementModel)? onTap;
const AchievementCarousel({super.key, required this.items, this.onTap});
const AchievementCarousel({super.key, required this.items, this.sectionConfig, this.onTap});
_handleTapRightButton() {
sectionConfig?.buttonViewAll?.directionalScreen?.begin();
}
@override
Widget build(BuildContext context) {
final width = MediaQuery.of(context).size.width;
if (items.isEmpty) return const SizedBox.shrink();
return SizedBox(
height: width*180/230/1.6,
child: ListView.separated(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 16),
itemCount: items.length,
separatorBuilder: (_, __) => const SizedBox(width: 12),
itemBuilder: (context, index) => AchievementCard(
achievement: items[index],
onTap: () => onTap?.call(items[index]),
return Column(
children: [
if ((sectionConfig?.name ?? "").isNotEmpty)
HeaderSectionTitle(
title: sectionConfig?.name?? "",
onViewAll: sectionConfig?.buttonViewAll?.directionalScreen != null ? _handleTapRightButton : null,
),
SizedBox(
height: width*180/230/1.6,
child: ListView.separated(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 16),
itemCount: items.length,
separatorBuilder: (_, __) => const SizedBox(width: 12),
itemBuilder: (context, index) => AchievementCard(
achievement: items[index],
onTap: () => onTap?.call(items[index]),
),
),
),
),
],
);
}
}
......
import 'package:flutter/material.dart';
import 'package:mypoint_flutter_app/extensions/string_extension.dart';
import '../../shopping/model/affiliate_brand_model.dart';
import '../../voucher/sub_widget/voucher_section_title.dart';
import '../models/main_section_config_model.dart';
class AffiliateBrandGridWidget extends StatelessWidget {
final List<AffiliateBrandModel> affiliateBrands;
final MainSectionConfigModel? sectionConfig;
final void Function(AffiliateBrandModel)? onTap;
const AffiliateBrandGridWidget({super.key, required this.affiliateBrands, this.sectionConfig, this.onTap});
_handleTapRightButton() {
sectionConfig?.buttonViewAll?.directionalScreen?.begin();
}
@override
Widget build(BuildContext context) {
if (affiliateBrands.isEmpty) {
return const SizedBox.shrink();
}
return Column(
children: [
if ((sectionConfig?.name ?? "").isNotEmpty)
HeaderSectionTitle(
title: sectionConfig?.name?? "",
onViewAll: sectionConfig?.buttonViewAll?.directionalScreen != null ? _handleTapRightButton : null,
),
GridView.builder(
padding: EdgeInsets.all(12),
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: affiliateBrands.length,
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
childAspectRatio: 3 / 3,
crossAxisSpacing: 12,
mainAxisSpacing: 12,
),
itemBuilder: (context, index) {
final brand = affiliateBrands[index];
return _buildAffiliateBrandItem(brand);
},
),
],
);
}
Widget _buildAffiliateBrandItem(AffiliateBrandModel brand) {
return LayoutBuilder(
builder: (context, constraints) {
final double imageWidth = constraints.maxWidth / 2.5;
return Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.08),
blurRadius: 5,
offset: const Offset(-2, 2),
)
],
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(
width: imageWidth,
height: imageWidth, // ✅ 1:1 tỉ lệ
child: ClipOval(
child: Image.network(
brand.logo ?? '',
fit: BoxFit.contain,
errorBuilder: (_, __, ___) => const Icon(Icons.broken_image),
),
),
),
const SizedBox(height: 2),
Text(
brand.brandName ?? "",
maxLines: 2,
textAlign: TextAlign.center,
overflow: TextOverflow.ellipsis,
style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 14),
),
const SizedBox(height: 4),
RichText(
textAlign: TextAlign.center,
text: TextSpan(
style: const TextStyle(fontSize: 12),
children: [
const TextSpan(
text: "Hoàn đến: ",
style: TextStyle(color: Colors.grey),
),
TextSpan(
text: "${(brand.commision ?? '').toNum()?.toString() ?? ''}%",
style: const TextStyle(
color: Colors.orange,
fontWeight: FontWeight.bold,
),
),
],
),
),
],
),
);
},
);
}
}
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:infinite_carousel/infinite_carousel.dart';
import '../../voucher/sub_widget/voucher_section_title.dart';
import '../models/banner_model.dart';
import '../models/main_section_config_model.dart';
class BannerCarousel extends StatefulWidget {
final List<String> imageUrls;
final List<BannerModel> banners;
final MainSectionConfigModel? sectionConfig;
final void Function(BannerModel)? onTap;
const BannerCarousel({super.key, required this.imageUrls});
const BannerCarousel({super.key, required this.banners, this.sectionConfig, this.onTap});
@override
State<BannerCarousel> createState() => _BannerCarouselState();
......@@ -49,66 +54,82 @@ class _BannerCarouselState extends State<BannerCarousel> {
_controller.dispose();
super.dispose();
}
//343/135
_handleTapRightButton() {
widget.sectionConfig?.buttonViewAll?.directionalScreen?.begin();
}
@override
Widget build(BuildContext context) {
final width = MediaQuery.of(context).size.width * 0.9;
return GestureDetector(
onPanDown: (_) => _pauseAutoPlayTemporarily(),
child: SizedBox(
height: width*135/343 + 16,
child: Stack(
alignment: Alignment.bottomCenter,
children: [
InfiniteCarousel.builder(
itemCount: widget.imageUrls.length,
itemExtent: width,
scrollBehavior: ScrollConfiguration.of(context).copyWith(scrollbars: false),
loop: true,
center: true,
anchor: 0.0,
velocityFactor: 0.1, // ✅ fix lỗi: snap từng page
controller: _controller,
onIndexChanged: (index) => setState(() => _currentIndex = index % widget.imageUrls.length),
itemBuilder: (context, itemIndex, realIndex) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 4.0, vertical: 8.0),
child: ClipRRect(
borderRadius: BorderRadius.circular(12),
child: Image.network(
widget.imageUrls[itemIndex % widget.imageUrls.length],
fit: BoxFit.cover,
width: double.infinity,
),
),
);
},
),
Positioned(
bottom: 12,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: widget.imageUrls.asMap().entries.map((entry) {
return GestureDetector(
onTap: () => _controller.animateToItem(entry.key, duration: const Duration(milliseconds: 500)),
child: Container(
width: 8.0,
height: 8.0,
margin: const EdgeInsets.symmetric(horizontal: 4.0),
decoration: BoxDecoration(
shape: BoxShape.circle,
color: _currentIndex == entry.key
? Colors.white
: Colors.white.withOpacity(0.4),
return Column(
children: [
if ((widget.sectionConfig?.name ?? "").isNotEmpty)
HeaderSectionTitle(
title: widget.sectionConfig?.name ?? "",
onViewAll: widget.sectionConfig?.buttonViewAll?.directionalScreen != null ? _handleTapRightButton : null,
),
GestureDetector(
onPanDown: (_) => _pauseAutoPlayTemporarily(),
child: SizedBox(
height: width * 135 / 343 + 16,
child: Stack(
alignment: Alignment.bottomCenter,
children: [
InfiniteCarousel.builder(
itemCount: widget.banners.length,
itemExtent: width,
scrollBehavior: ScrollConfiguration.of(context).copyWith(scrollbars: false),
loop: true,
center: true,
anchor: 0.0,
velocityFactor: 0.1, // ✅ fix lỗi: snap từng page
controller: _controller,
onIndexChanged: (index) => setState(() => _currentIndex = index % widget.banners.length),
itemBuilder: (context, itemIndex, realIndex) {
return GestureDetector(
onTap: () => widget.onTap?.call(widget.banners[itemIndex % widget.banners.length]),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 4.0, vertical: 8.0),
child: ClipRRect(
borderRadius: BorderRadius.circular(12),
child: Image.network(
widget.banners[itemIndex % widget.banners.length].itemImage ?? "",
fit: BoxFit.cover,
width: double.infinity,
),
),
),
),
);
}).toList(),
),
);
},
),
Positioned(
bottom: 12,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children:
widget.banners.asMap().entries.map((entry) {
return GestureDetector(
onTap:
() => _controller.animateToItem(entry.key, duration: const Duration(milliseconds: 500)),
child: Container(
width: 8.0,
height: 8.0,
margin: const EdgeInsets.symmetric(horizontal: 4.0),
decoration: BoxDecoration(
shape: BoxShape.circle,
color: _currentIndex == entry.key ? Colors.white : Colors.white.withOpacity(0.4),
),
),
);
}).toList(),
),
),
],
),
],
),
),
),
],
);
}
}
\ No newline at end of file
}
import 'package:flutter/material.dart';
import 'package:mypoint_flutter_app/extensions/string_extension.dart';
import 'package:mypoint_flutter_app/widgets/image_loader.dart';
import '../../shopping/model/affiliate_brand_model.dart';
import '../../voucher/sub_widget/voucher_section_title.dart';
import '../models/brand_model.dart';
import '../models/main_section_config_model.dart';
class BrandGridWidget extends StatelessWidget {
final List<BrandModel> brands;
final MainSectionConfigModel? sectionConfig;
final void Function(BrandModel)? onTap;
const BrandGridWidget({super.key, required this.brands, this.sectionConfig, this.onTap});
_handleTapRightButton() {
sectionConfig?.buttonViewAll?.directionalScreen?.begin();
}
@override
Widget build(BuildContext context) {
if (brands.isEmpty) {
return const SizedBox.shrink();
}
return Column(
children: [
if ((sectionConfig?.name ?? "").isNotEmpty)
HeaderSectionTitle(
title: sectionConfig?.name ?? "",
onViewAll: sectionConfig?.buttonViewAll?.directionalScreen != null ? _handleTapRightButton : null,
),
GridView.builder(
padding: EdgeInsets.all(0),
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: brands.length,
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
childAspectRatio: 3 / 3,
crossAxisSpacing: 0,
mainAxisSpacing: 0,
),
itemBuilder: (context, index) {
final brand = brands[index];
return _buildBrandItem(brand, index);
},
),
],
);
}
Widget _buildBrandItem(BrandModel brand, int index) {
return LayoutBuilder(
builder: (context, constraints) {
final double imageWidth = constraints.maxWidth / 3;
return Container(
padding: const EdgeInsets.all(4),
color: index % 2 != 0 ? Colors.red.shade50 : Colors.white,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ClipRRect(
borderRadius: BorderRadius.circular(8),
child: SizedBox(
width: imageWidth,
height: imageWidth,
child: loadNetworkImage(
url: brand.logo ?? "",
fit: BoxFit.contain,
placeholderAsset: "assets/images/bg_default_11.png",
),
),
),
const SizedBox(height: 4),
Text(
textAlign: TextAlign.center,
brand.brandName ?? "",
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 14),
),
const SizedBox(height: 4),
RichText(
textAlign: TextAlign.center,
text: TextSpan(
style: const TextStyle(fontSize: 12),
children: [
const TextSpan(text: "Hoàn đến: ", style: TextStyle(color: Colors.grey)),
TextSpan(
text: brand.pointAccumulationRate ?? '',
style: const TextStyle(color: Colors.orange, fontWeight: FontWeight.bold),
),
],
),
),
],
),
);
},
);
}
}
import 'package:flutter/material.dart';
import 'package:mypoint_flutter_app/extensions/num_extension.dart';
import '../../voucher/models/product_model.dart';
import '../../voucher/sub_widget/voucher_section_title.dart';
import '../models/main_section_config_model.dart';
import 'flash_sale_header_widget.dart';
class FlashSaleCarouselWidget extends StatefulWidget {
final List<ProductModel> products;
final MainSectionConfigModel? sectionConfig;
final void Function(ProductModel)? onTap;
const FlashSaleCarouselWidget({super.key, required this.products, this.sectionConfig, this.onTap});
@override
State<FlashSaleCarouselWidget> createState() => _FlashSaleCarouselWidgetState();
}
class _FlashSaleCarouselWidgetState extends State<FlashSaleCarouselWidget> {
_handleTapRightButton() {
widget.sectionConfig?.buttonViewAll?.directionalScreen?.begin();
}
@override
Widget build(BuildContext context) {
final widthItem = MediaQuery.of(context).size.width / 2.5;
final heightItem = widthItem * 9 / 16 + (widget.products.firstOrNull?.extendSpaceFlashSaleItem ?? 130);
if (widget.products.isEmpty) return const SizedBox.shrink();
return Column(
children: [
if (widget.sectionConfig?.flashSale != null)
FlashSaleHeader(
flashSale: widget.sectionConfig?.flashSale,
onViewAll: widget.sectionConfig?.buttonViewAll?.directionalScreen != null ? _handleTapRightButton : null,
),
const SizedBox(height: 8),
ConstrainedBox(
constraints: BoxConstraints(maxHeight: heightItem),
child: ListView.separated(
shrinkWrap: true,
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 16),
itemCount: widget.products.length,
separatorBuilder: (_, __) => const SizedBox(width: 12),
itemBuilder: (context, index) => _buildFlashSaleGridItem(widget.products[index]),
),
),
],
);
}
Widget _buildFlashSaleGridItem(ProductModel product) {
final widthItem = MediaQuery.of(context).size.width / 2.5;
return GestureDetector(
onTap: () => widget.onTap?.call(product),
child: Container(
width: widthItem,
margin: const EdgeInsets.symmetric(horizontal: 0),
padding: const EdgeInsets.all(0),
decoration: BoxDecoration(
border: Border.all(color: Colors.grey.shade200),
borderRadius: BorderRadius.circular(12),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Hình ảnh + tag % giảm
Stack(
children: [
ClipRRect(
borderRadius: BorderRadius.only(topLeft: Radius.circular(8), topRight: Radius.circular(8)),
child: Image.network(
product.banner?.url ?? '',
width: double.infinity,
height: widthItem * 9 / 16,
fit: BoxFit.cover,
),
),
if (product.percentDiscount != null)
Positioned(
right: 4,
bottom: 4,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(color: Colors.red, borderRadius: BorderRadius.circular(20)),
child: Text(
'-${product.percentDiscount}%',
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 12),
),
),
),
],
),
// const SizedBox(height: 8),
// Giá khuyến mãi + gạch giá gốc
Container(
padding: const EdgeInsets.all(4),
child: Column(
children: [
SizedBox(
height: 24,
child: Row(
children: [
Image.asset("assets/images/ic_hot_flash_sale.png", width: 20, height: 20, fit: BoxFit.cover),
// if (product.previewFlashSale?.price != null)
Text(
"${product.amountToBePaid?.money(CurrencyUnit.noneSpace)}đ",
// '${product.previewFlashSale?.price}đ',
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
const SizedBox(width: 2),
if (product.price?.value != null)
Text(
'${product.price?.value?.money(CurrencyUnit.noneSpace)}đ',
style: const TextStyle(
fontSize: 12,
color: Colors.grey,
decoration: TextDecoration.lineThrough,
),
),
],
),
),
const SizedBox(height: 4),
Text(
product.name ?? '',
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: const TextStyle(fontSize: 13),
),
const SizedBox(height: 4),
// Button Nhận quà / điểm
if (product.previewFlashSale?.rewardContent != null)
Align(
alignment: Alignment.centerLeft,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
border: Border.all(color: Colors.orange),
borderRadius: BorderRadius.circular(20),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Image.asset(
product.previewFlashSale?.imageReward ?? 'assets/images/ic_gift_flash_sale.png',
width: 16,
height: 16,
color: Colors.orange,
),
const SizedBox(width: 4),
Text(
product.previewFlashSale?.rewardContent ?? 'Nhận quà',
style: const TextStyle(fontSize: 12, color: Colors.orange, fontWeight: FontWeight.bold),
),
],
),
),
),
// Thanh tiến trình + Số lượng đã bán
if (product.isShowProsessSoldItem)
Column(
children: [
const SizedBox(height: 4),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
// Thanh tiến trình
SizedBox(
width: widthItem - 90,
child: ClipRRect(
borderRadius: BorderRadius.circular(4),
child: LinearProgressIndicator(
value: product.progress,
minHeight: 6,
backgroundColor: Colors.grey.shade300,
valueColor: AlwaysStoppedAnimation<Color>(
product.inStock ? Colors.orange : Colors.red,
),
),
),
),
const SizedBox(width: 2),
Text(
product.inStock
? 'Đã bán ${product.previewFlashSale?.fsQuantitySold ?? 0}'
: 'Đã bán hết',
style: TextStyle(fontSize: 12, color: product.inStock ? Colors.black : Colors.grey),
),
],
),
],
),
],
),
),
],
),
),
);
}
}
import 'dart:async';
import 'package:flutter/material.dart';
import '../../flash_sale/preview_flash_sale_model.dart';
class FlashSaleHeader extends StatefulWidget {
final PreviewFlashSale? flashSale;
final VoidCallback? onViewAll;
const FlashSaleHeader({
super.key,
required this.flashSale,
this.onViewAll,
});
@override
State<FlashSaleHeader> createState() => _FlashSaleHeaderState();
}
class _FlashSaleHeaderState extends State<FlashSaleHeader> {
late Duration _remaining;
Timer? _timer;
@override
void initState() {
super.initState();
_remaining = widget.flashSale?.countdownLocal ?? Duration(seconds: 100000);
_startTimer();
}
void _startTimer() {
_timer?.cancel();
_timer = Timer.periodic(const Duration(seconds: 1), (_) {
if (_remaining.inSeconds <= 0) {
_timer?.cancel();
} else {
setState(() {
_remaining -= const Duration(seconds: 1);
});
}
});
}
String _formatTime(int value) => value.toString().padLeft(2, '0');
Widget _buildTimeBox(String text) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 3, vertical: 2),
decoration: BoxDecoration(
color: Colors.red.shade400,
borderRadius: BorderRadius.circular(4),
),
child: Text(
text,
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
),
);
}
@override
void dispose() {
_timer?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
final hours = _formatTime(_remaining.inHours);
final minutes = _formatTime(_remaining.inMinutes.remainder(60));
final seconds = _formatTime(_remaining.inSeconds.remainder(60));
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Row(
children: [
Image.asset("assets/images/ic_flash_sale.png", height: 24, fit: BoxFit.cover,),
const SizedBox(width: 6),
Text(widget.flashSale?.desTime ?? "", style: TextStyle(fontSize: 14)),
const SizedBox(width: 4),
_buildTimeBox(hours),
const SizedBox(width: 2),
const Text(":", style: TextStyle(fontWeight: FontWeight.bold)),
const SizedBox(width: 2),
_buildTimeBox(minutes),
const SizedBox(width: 2),
const Text(":", style: TextStyle(fontWeight: FontWeight.bold)),
const SizedBox(width: 2),
_buildTimeBox(seconds),
const Spacer(),
if (widget.onViewAll != null)
GestureDetector(
onTap: widget.onViewAll,
child: Text(
'Xem tất cả',
style: TextStyle(color: Colors.blue[700], fontWeight: FontWeight.bold),
),
),
],
),
);
}
}
......@@ -4,41 +4,38 @@ import 'package:mypoint_flutter_app/widgets/image_loader.dart';
import '../../../preference/data_preference.dart';
import '../../../preference/point/header_home_model.dart';
import '../../../shared/router_gage.dart';
import '../models/notification_unread_model.dart';
class HomeGreetingHeader extends StatelessWidget {
final double? heightContent;
HeaderHomeModel dataHeader;
String level = "Hạng Đồng";
NotificationUnreadData? notificationUnreadData;
HomeGreetingHeader({
super.key,
this.heightContent,
required this.dataHeader,
});
HomeGreetingHeader({super.key, this.heightContent, required this.dataHeader, required this.notificationUnreadData});
@override
Widget build(BuildContext context) {
final width = MediaQuery.of(context).size.width;
final heightSize = heightContent ?? (width * 86 / 375 + 112);
final name = DataPreference.instance.profile?.workingSite?.name ?? "Quý Khách";
double heightWhiteBox = 112;
final name = DataPreference.instance.fullName;
final level = DataPreference.instance.rankName ?? "Hạng Đồng";
double heightWhiteBox = 112;
return Stack(
children: [
Container(
SizedBox(
height: heightSize,
width: double.infinity,
decoration: const BoxDecoration(
image: DecorationImage(image: AssetImage('assets/images/bg_header_navi.png'), fit: BoxFit.cover),
child: loadNetworkImage(
url: dataHeader.background,
fit: BoxFit.cover,
placeholderAsset: "assets/images/bg_header_navi.png",
),
// child: loadNetworkImage(url: dataHeader.background, fit: BoxFit.cover, placeholderAsset: "assets/images/bg_header_navi.png"),
),
Positioned(
bottom: heightWhiteBox + 16,
left: 16,
child: Image.asset(
'assets/images/ic_logo_mypoint.png',
height: 24,
),
child: Image.asset('assets/images/ic_logo_mypoint.png', height: 24),
),
Positioned(
left: 0,
......@@ -79,7 +76,7 @@ class HomeGreetingHeader extends StatelessWidget {
child: Stack(
children: [
Image.asset('assets/images/ic_notify_black.png', width: 32, height: 32),
if (1 > 0)
if ((notificationUnreadData?.unread ?? 0) > 0)
Positioned(
right: 6,
top: 4,
......@@ -100,7 +97,11 @@ class HomeGreetingHeader extends StatelessWidget {
const SizedBox(height: 2),
Row(
children: [
_buildStatItem(icon: "assets/images/ic_point_gray.png", value: dataHeader.totalPointActive.toString(), onTap: _onPointTap),
_buildStatItem(
icon: "assets/images/ic_point_gray.png",
value: dataHeader.totalPointActive.toString(),
onTap: _onPointTap,
),
SizedBox(width: 12),
_buildStatItem(
icon: "assets/images/ic_voucher_gray.png",
......@@ -124,10 +125,7 @@ class HomeGreetingHeader extends StatelessWidget {
onTap: onTap,
child: Container(
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 4),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
border: Border.all(color: Colors.black26),
),
decoration: BoxDecoration(borderRadius: BorderRadius.circular(20), border: Border.all(color: Colors.black26)),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
......@@ -153,10 +151,12 @@ class HomeGreetingHeader extends StatelessWidget {
}
_onMyVoucherTap() {
Get.toNamed(myVoucherListScreen);
print("_onMyVoucherTap");
}
_onRankTap() {
Get.toNamed(membershipScreen);
print("_onRankTap");
}
}
......@@ -99,17 +99,7 @@ class _HomeScreenWithHeaderState extends State<HomeScreenWithHeader> {
color: Colors.white,
child: Column(
children: [
BannerCarousel(
imageUrls: [
'https://api.mypoint.com.vn/8854/gup2start/rest/photoReader/1.0.0/F31FF2E775D7BFC940156709FB79E883/1746430303',
'https://api.mypoint.com.vn/8854/gup2start/rest/photoReader/1.0.0/1B67CFBF96BD24929EB10F1853A47651/1740708747',
'https://api.mypoint.com.vn/8854/gup2start/rest/photoReader/1.0.0/29B40B1E04EBEC8A1C1F9D13C0194A27/1735194572',
'https://api.mypoint.com.vn/8854/gup2start/rest/photoReader/1.0.0/C29872C4F95B280B880DE45BC07E7DE4/1693906872',
'https://api.mypoint.com.vn/8854/gup2start/rest/photoReader/1.0.0/F31FF2E775D7BFC940156709FB79E883/1746430303',
'https://api.mypoint.com.vn/8854/gup2start/rest/photoReader/1.0.0/F31FF2E775D7BFC940156709FB79E883/1746430303',
'https://api.mypoint.com.vn/8854/gup2start/rest/photoReader/1.0.0/F31FF2E775D7BFC940156709FB79E883/1746430303',
],
),
BannerCarousel(banners: [],),
if (_services.isNotEmpty)
MainServiceGrid(
services: _services,
......
import 'package:flutter/material.dart';
import '../../../widgets/image_loader.dart';
import '../../voucher/sub_widget/voucher_section_title.dart';
import '../models/main_section_config_model.dart';
import '../models/main_service_model.dart';
class MainServiceGrid extends StatelessWidget {
final List<MainServiceModel> services;
final MainSectionConfigModel? sectionConfig;
final void Function(MainServiceModel)? onTap;
const MainServiceGrid({super.key, required this.services, this.onTap});
_handleTapRightButton() {
sectionConfig?.buttonViewAll?.directionalScreen?.begin();
}
const MainServiceGrid({super.key, required this.services, this.onTap, this.sectionConfig});
@override
Widget build(BuildContext context) {
return SizedBox(
height: 120,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: ListView.separated(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 16),
itemCount: services.length,
separatorBuilder: (_, __) => const SizedBox(width: 16),
itemBuilder: (context, index) => _buildItem(context, services[index]),
return Column(
children: [
if ((sectionConfig?.name ?? "").isNotEmpty)
HeaderSectionTitle(
title: sectionConfig?.name?? "",
onViewAll: sectionConfig?.buttonViewAll?.directionalScreen != null ? _handleTapRightButton : null,
),
SizedBox(
height: 120,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: ListView.separated(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 16),
itemCount: services.length,
separatorBuilder: (_, __) => const SizedBox(width: 16),
itemBuilder: (context, index) => _buildItem(context, services[index]),
),
),
),
),
],
);
}
......
import 'package:flutter/material.dart';
import 'package:mypoint_flutter_app/widgets/image_loader.dart';
import '../../voucher/sub_widget/voucher_section_title.dart';
import '../models/main_section_config_model.dart';
import '../models/my_product_model.dart';
class MyProductCarouselWidget extends StatelessWidget {
final List<MyProductModel> items;
final MainSectionConfigModel? sectionConfig;
final void Function(MyProductModel)? onTap;
const MyProductCarouselWidget({super.key, required this.items, this.sectionConfig, this.onTap});
_handleTapRightButton() {
sectionConfig?.buttonViewAll?.directionalScreen?.begin();
}
@override
Widget build(BuildContext context) {
final widthItem = MediaQuery.of(context).size.width / 1.2;
if (items.isEmpty) return const SizedBox.shrink();
return Container(
color: Colors.white,
child: Column(
children: [
if ((sectionConfig?.name ?? "").isNotEmpty)
HeaderSectionTitle(
title: sectionConfig?.name ?? "",
onViewAll: sectionConfig?.buttonViewAll?.directionalScreen != null ? _handleTapRightButton : null,
),
SizedBox(
height: widthItem * 99 / 280 + 24,
child: ListView.separated(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 16),
itemCount: items.length,
separatorBuilder: (_, __) => const SizedBox(width: 12),
itemBuilder: (context, index) => _buildItem(context, items[index], widthItem),
),
),
],
),
);
}
Widget _buildItem(BuildContext context, MyProductModel product, double widthItem) {
return GestureDetector(
onTap: () => onTap?.call(product),
child: Container(
width: widthItem,
margin: const EdgeInsets.symmetric(vertical: 16, horizontal: 0),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white,
border: Border.all(color: Colors.grey.shade200),
borderRadius: BorderRadius.circular(12),
boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.05), blurRadius: 6, offset: const Offset(0, 2))],
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
ClipRRect(
borderRadius: BorderRadius.circular(8),
child: loadNetworkImage(
url: product.logo ?? '',
width: 64,
height: 64,
fit: BoxFit.cover,
placeholderAsset: "assets/images/bg_default_11.png",
),
),
const SizedBox(width: 16),
Expanded(
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
product.brandName ?? '',
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
maxLines: 1,
),
const SizedBox(height: 2),
Text(
product.title ?? '',
style: const TextStyle(fontSize: 13, color: Colors.black87),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 2),
Text('HSD: ${product.expire ?? ''}', style: const TextStyle(fontSize: 13, color: Colors.black54)),
],
),
),
),
],
),
),
);
}
}
......@@ -2,25 +2,41 @@ import 'package:flutter/material.dart';
import 'package:mypoint_flutter_app/widgets/image_loader.dart';
import '../../faqs/faqs_model.dart';
import '../../voucher/sub_widget/voucher_section_title.dart';
import '../models/main_section_config_model.dart';
class NewsCarouselWidget extends StatelessWidget {
final List<PageItemModel> items;
final MainSectionConfigModel? sectionConfig;
final void Function(PageItemModel)? onTap;
const NewsCarouselWidget({super.key, required this.items, this.onTap});
const NewsCarouselWidget({super.key, required this.items, this.sectionConfig, this.onTap});
_handleTapRightButton() {
sectionConfig?.buttonViewAll?.directionalScreen?.begin();
}
@override
Widget build(BuildContext context) {
final widthItem = MediaQuery.of(context).size.width/1.6;
if (items.isEmpty) return const SizedBox.shrink();
return SizedBox(
height: widthItem*9/16 + 72,
child: ListView.separated(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 16),
itemCount: items.length,
separatorBuilder: (_, __) => const SizedBox(width: 12),
itemBuilder: (context, index) => _buildItem(context, items[index]),
),
return Column(
children: [
if ((sectionConfig?.name ?? "").isNotEmpty)
HeaderSectionTitle(
title: sectionConfig?.name?? "",
onViewAll: sectionConfig?.buttonViewAll?.directionalScreen != null ? _handleTapRightButton : null,
),
SizedBox(
height: widthItem*9/16 + 72,
child: ListView.separated(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 16),
itemCount: items.length,
separatorBuilder: (_, __) => const SizedBox(width: 12),
itemBuilder: (context, index) => _buildItem(context, items[index]),
),
),
],
);
}
......
import 'package:flutter/material.dart';
import 'package:mypoint_flutter_app/widgets/image_loader.dart';
import '../../../widgets/custom_point_text_tag.dart';
import '../../voucher/models/product_model.dart';
import '../../voucher/sub_widget/voucher_section_title.dart';
import '../models/main_section_config_model.dart';
class ProductGrid extends StatelessWidget {
final List<ProductModel> products;
final MainSectionConfigModel? sectionConfig;
final void Function(ProductModel)? onTap;
final double _spacing = 12;
const ProductGrid({super.key, required this.products, this.onTap});
const ProductGrid({super.key, required this.products, this.sectionConfig, this.onTap});
_handleTapRightButton() {
sectionConfig?.buttonViewAll?.directionalScreen?.begin();
}
@override
Widget build(BuildContext context) {
final width = MediaQuery.of(context).size.width;
final widthItem = (width - _spacing * 3)/2;
return GridView.builder(
physics: const NeverScrollableScrollPhysics(),
shrinkWrap: true,
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 16),
itemCount: products.length,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
childAspectRatio: widthItem / (widthItem * 9/16 + 94),
mainAxisSpacing: _spacing,
crossAxisSpacing: _spacing,
),
itemBuilder: (context, index) => _buildItem(context, products[index]),
return Column(
children: [
if ((sectionConfig?.name ?? "").isNotEmpty)
HeaderSectionTitle(
title: sectionConfig?.name?? "",
onViewAll: sectionConfig?.buttonViewAll?.directionalScreen != null ? _handleTapRightButton : null,
),
GridView.builder(
physics: const NeverScrollableScrollPhysics(),
shrinkWrap: true,
padding: EdgeInsets.only(left: 16, right: 16, top: 8, bottom: 24),//EdgeInsets.symmetric(horizontal: 16, vertical: 16),
itemCount: products.length,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
childAspectRatio: widthItem / (widthItem * 9/16 + 94),
mainAxisSpacing: _spacing,
crossAxisSpacing: _spacing,
),
itemBuilder: (context, index) => _buildItem(context, products[index]),
),
],
);
}
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment