Skip to content
GitLab
Menu
Projects
Groups
Snippets
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
Menu
Open sidebar
Hoàng Văn Đạt
mypoint_flutter_app
Commits
fda33894
Commit
fda33894
authored
Jun 06, 2025
by
DatHV
Browse files
cap nhat giao dien
parent
75178f29
Changes
86
Show whitespace changes
Inline
Side-by-side
lib/screen/faqs/faqs_model.dart
View file @
fda33894
...
...
@@ -2,7 +2,7 @@ import 'package:json_annotation/json_annotation.dart';
part
'faqs_model.g.dart'
;
@JsonSerializable
()
class
FAQ
ItemModel
{
class
Page
ItemModel
{
final
String
?
thumbnail
;
@JsonKey
(
name:
"page_id"
)
final
String
?
pageId
;
...
...
@@ -11,7 +11,7 @@ class FAQItemModel {
final
String
?
publishAtDate
;
final
String
?
chapeau
;
FAQ
ItemModel
({
Page
ItemModel
({
this
.
thumbnail
,
this
.
pageId
,
this
.
title
,
...
...
@@ -19,13 +19,13 @@ class FAQItemModel {
this
.
chapeau
,
});
factory
FAQ
ItemModel
.
fromJson
(
Map
<
String
,
dynamic
>
json
)
=>
_$
FAQ
ItemModelFromJson
(
json
);
Map
<
String
,
dynamic
>
toJson
()
=>
_$
FAQ
ItemModelToJson
(
this
);
factory
Page
ItemModel
.
fromJson
(
Map
<
String
,
dynamic
>
json
)
=>
_$
Page
ItemModelFromJson
(
json
);
Map
<
String
,
dynamic
>
toJson
()
=>
_$
Page
ItemModelToJson
(
this
);
}
@JsonSerializable
()
class
FAQItemModelResponse
{
final
List
<
FAQ
ItemModel
>?
items
;
final
List
<
Page
ItemModel
>?
items
;
FAQItemModelResponse
({
this
.
items
,
...
...
lib/screen/faqs/faqs_model.g.dart
View file @
fda33894
...
...
@@ -6,15 +6,16 @@ part of 'faqs_model.dart';
// JsonSerializableGenerator
// **************************************************************************
FAQItemModel
_$FAQItemModelFromJson
(
Map
<
String
,
dynamic
>
json
)
=>
FAQItemModel
(
PageItemModel
_$PageItemModelFromJson
(
Map
<
String
,
dynamic
>
json
)
=>
PageItemModel
(
thumbnail:
json
[
'thumbnail'
]
as
String
?,
pageId:
json
[
'page_id'
]
as
String
?,
title:
json
[
'title'
]
as
String
?,
publishAtDate:
json
[
'publish_at_date'
]
as
String
?,
chapeau:
json
[
'chapeau'
]
as
String
?,
);
);
Map
<
String
,
dynamic
>
_$
FAQ
ItemModelToJson
(
FAQ
ItemModel
instance
)
=>
Map
<
String
,
dynamic
>
_$
Page
ItemModelToJson
(
Page
ItemModel
instance
)
=>
<
String
,
dynamic
>{
'thumbnail'
:
instance
.
thumbnail
,
'page_id'
:
instance
.
pageId
,
...
...
@@ -28,7 +29,7 @@ FAQItemModelResponse _$FAQItemModelResponseFromJson(
)
=>
FAQItemModelResponse
(
items:
(
json
[
'items'
]
as
List
<
dynamic
>?)
?.
map
((
e
)
=>
FAQ
ItemModel
.
fromJson
(
e
as
Map
<
String
,
dynamic
>))
?.
map
((
e
)
=>
Page
ItemModel
.
fromJson
(
e
as
Map
<
String
,
dynamic
>))
.
toList
(),
);
...
...
lib/screen/faqs/faqs_screen.dart
View file @
fda33894
...
...
@@ -6,6 +6,7 @@ import 'package:mypoint_flutter_app/widgets/back_button.dart';
import
'../../base/base_screen.dart'
;
import
'../../base/basic_state.dart'
;
import
'../../resouce/base_color.dart'
;
import
'../../shared/router_gage.dart'
;
import
'faqs_viewmodel.dart'
;
class
FAQScreen
extends
BaseScreen
{
...
...
@@ -52,7 +53,7 @@ class _FAQScreenState extends BaseState<FAQScreen> with BasicState {
return
GestureDetector
(
onTap:
()
{
if
(
item
.
pageId
!=
null
&&
item
.
pageId
!.
isNotEmpty
)
{
Get
.
to
(()
=>
C
ampaignDetailScreen
(
pageId
:
item
.
pageId
)
);
Get
.
to
Named
(
c
ampaignDetailScreen
,
arguments:
{
"id"
:
item
.
pageId
}
);
}
else
{
Get
.
snackbar
(
"Thông báo"
,
...
...
lib/screen/faqs/faqs_viewmodel.dart
View file @
fda33894
...
...
@@ -5,7 +5,7 @@ import '../../base/restful_api_viewmodel.dart';
import
'faqs_model.dart'
;
class
FAQViewModel
extends
RestfulApiViewModel
{
var
faqItems
=
<
FAQ
ItemModel
>[].
obs
;
var
faqItems
=
<
Page
ItemModel
>[].
obs
;
var
isLoading
=
true
.
obs
;
@override
...
...
@@ -17,7 +17,7 @@ class FAQViewModel extends RestfulApiViewModel {
Future
<
void
>
fetchFAQItems
()
async
{
showLoading
();
isLoading
(
true
);
client
.
websiteFolderGetPageList
().
then
((
value
)
{
client
.
websiteFolderGetPageList
(
{
"folder_uri"
:
"FAQ"
}
).
then
((
value
)
{
hideLoading
();
isLoading
(
false
);
faqItems
.
value
=
value
.
data
?.
items
??
[];
...
...
lib/screen/flash_sale/preview_flash_sale_model.dart
View file @
fda33894
...
...
@@ -32,7 +32,7 @@ class PreviewFlashSale {
@JsonKey
(
name:
'is_flash_sale_price'
)
final
bool
?
isFlashSalePrice
;
@JsonKey
(
name:
'header_img'
)
final
String
?
headerImg
;
String
?
headerImg
;
@JsonKey
(
name:
'fs_quantity_per_person_total'
)
final
int
?
fsQuantityPerPersonTotal
;
@JsonKey
(
name:
'fs_quantity_per_person_bought'
)
...
...
lib/screen/home/custom_widget/achievement_carousel_widget.dart
0 → 100644
View file @
fda33894
import
'package:flutter/cupertino.dart'
;
import
'../../../widgets/image_loader.dart'
;
import
'../models/achievement_model.dart'
;
class
AchievementCarousel
extends
StatelessWidget
{
final
List
<
AchievementModel
>
items
;
final
void
Function
(
AchievementModel
)?
onTap
;
const
AchievementCarousel
({
super
.
key
,
required
this
.
items
,
this
.
onTap
});
@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
]),
),
),
);
}
}
class
AchievementCard
extends
StatelessWidget
{
final
AchievementModel
achievement
;
final
void
Function
()?
onTap
;
const
AchievementCard
({
super
.
key
,
required
this
.
achievement
,
this
.
onTap
});
@override
Widget
build
(
BuildContext
context
)
{
final
imageUrl
=
achievement
.
images
?.
first
.
imageUrl
;
return
GestureDetector
(
onTap:
onTap
,
child:
ClipRRect
(
borderRadius:
BorderRadius
.
circular
(
12
),
child:
loadNetworkImage
(
url:
imageUrl
,
fit:
BoxFit
.
cover
,
width:
280
,
height:
140
,
placeholderAsset:
'assets/images/ic_logo.png'
,
),
),
);
}
}
lib/screen/home/custom_widget/banner_carousel_widget.dart
0 → 100644
View file @
fda33894
import
'dart:async'
;
import
'package:flutter/material.dart'
;
import
'package:infinite_carousel/infinite_carousel.dart'
;
class
BannerCarousel
extends
StatefulWidget
{
final
List
<
String
>
imageUrls
;
const
BannerCarousel
({
super
.
key
,
required
this
.
imageUrls
});
@override
State
<
BannerCarousel
>
createState
()
=>
_BannerCarouselState
();
}
class
_BannerCarouselState
extends
State
<
BannerCarousel
>
{
late
InfiniteScrollController
_controller
;
int
_currentIndex
=
0
;
Timer
?
_autoPlayTimer
;
final
bool
_isDragging
=
false
;
bool
_isUserScrolling
=
false
;
@override
void
initState
()
{
super
.
initState
();
_controller
=
InfiniteScrollController
(
initialItem:
0
);
_startAutoPlay
();
}
void
_startAutoPlay
()
{
_autoPlayTimer
?.
cancel
();
_autoPlayTimer
=
Timer
.
periodic
(
const
Duration
(
seconds:
2
),
(
_
)
{
if
(!
_isDragging
&&
!
_isUserScrolling
)
{
_controller
.
nextItem
(
duration:
const
Duration
(
milliseconds:
500
));
}
});
}
void
_pauseAutoPlayTemporarily
()
{
_isUserScrolling
=
true
;
_autoPlayTimer
?.
cancel
();
Future
.
delayed
(
const
Duration
(
seconds:
1
),
()
{
_isUserScrolling
=
false
;
_startAutoPlay
();
});
}
@override
void
dispose
()
{
_autoPlayTimer
?.
cancel
();
_controller
.
dispose
();
super
.
dispose
();
}
//343/135
@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
),
),
),
);
}).
toList
(),
),
),
],
),
),
);
}
}
\ No newline at end of file
lib/screen/home/custom_widget/header_home.dart
0 → 100644
View file @
fda33894
import
'package:flutter/material.dart'
;
import
'package:get/get.dart'
;
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'
;
class
HomeGreetingHeader
extends
StatelessWidget
{
final
double
?
heightContent
;
HeaderHomeModel
dataHeader
;
String
level
=
"Hạng Đồng"
;
HomeGreetingHeader
({
super
.
key
,
this
.
heightContent
,
required
this
.
dataHeader
,
});
@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
;
return
Stack
(
children:
[
Container
(
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"),
),
Positioned
(
bottom:
heightWhiteBox
+
16
,
left:
16
,
child:
Image
.
asset
(
'assets/images/ic_logo_mypoint.png'
,
height:
24
,
),
),
Positioned
(
left:
0
,
right:
0
,
bottom:
0
,
child:
Container
(
height:
heightWhiteBox
,
padding:
const
EdgeInsets
.
symmetric
(
horizontal:
16
,
vertical:
8
),
decoration:
BoxDecoration
(
color:
Colors
.
white
,
borderRadius:
BorderRadius
.
only
(
topRight:
Radius
.
circular
(
16
),
topLeft:
Radius
.
circular
(
16
),
),
//.circular(20),
),
child:
Column
(
children:
[
Row
(
mainAxisAlignment:
MainAxisAlignment
.
spaceBetween
,
children:
[
Text
(
'Xin chào
$name
!'
,
style:
TextStyle
(
fontSize:
18
,
fontWeight:
FontWeight
.
bold
,
color:
Colors
.
black87
),
),
Row
(
children:
[
GestureDetector
(
onTap:
_onSearchTap
,
child:
Container
(
padding:
EdgeInsets
.
all
(
8
),
child:
Image
.
asset
(
'assets/images/ic_search_black.png'
,
width:
32
,
height:
32
),
),
),
GestureDetector
(
onTap:
_onNotificationTap
,
child:
Container
(
padding:
EdgeInsets
.
all
(
8
),
child:
Stack
(
children:
[
Image
.
asset
(
'assets/images/ic_notify_black.png'
,
width:
32
,
height:
32
),
if
(
1
>
0
)
Positioned
(
right:
6
,
top:
4
,
child:
Container
(
height:
8
,
width:
8
,
decoration:
BoxDecoration
(
color:
Colors
.
red
,
shape:
BoxShape
.
circle
),
),
),
],
),
),
),
],
),
],
),
const
SizedBox
(
height:
2
),
Row
(
children:
[
_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"
,
value:
dataHeader
.
totalVoucher
.
toString
(),
onTap:
_onMyVoucherTap
,
),
SizedBox
(
width:
12
),
_buildStatItem
(
icon:
"assets/images/ic_rank_gray.png"
,
value:
level
,
onTap:
_onRankTap
),
],
),
],
),
),
),
],
);
}
Widget
_buildStatItem
({
required
String
icon
,
required
String
value
,
VoidCallback
?
onTap
})
{
return
GestureDetector
(
onTap:
onTap
,
child:
Container
(
padding:
EdgeInsets
.
symmetric
(
horizontal:
12
,
vertical:
4
),
decoration:
BoxDecoration
(
borderRadius:
BorderRadius
.
circular
(
20
),
border:
Border
.
all
(
color:
Colors
.
black26
),
),
child:
Row
(
mainAxisSize:
MainAxisSize
.
min
,
children:
[
Container
(
padding:
EdgeInsets
.
all
(
2
),
child:
Image
.
asset
(
icon
,
width:
24
,
height:
24
)),
SizedBox
(
width:
2
),
Text
(
value
,
style:
TextStyle
(
fontSize:
14
,
fontWeight:
FontWeight
.
w500
,
color:
Colors
.
black87
)),
],
),
),
);
}
_onSearchTap
()
{
Get
.
toNamed
(
vouchersScreen
,
arguments:
{
"enableSearch"
:
true
});
}
_onNotificationTap
()
{
Get
.
toNamed
(
notificationScreen
);
}
_onPointTap
()
{
print
(
"_onPointTap"
);
}
_onMyVoucherTap
()
{
print
(
"_onMyVoucherTap"
);
}
_onRankTap
()
{
print
(
"_onRankTap"
);
}
}
lib/screen/home/custom_widget/home_header_widget.dart
0 → 100644
View file @
fda33894
import
'dart:convert'
;
import
'package:flutter/material.dart'
;
import
'package:flutter/services.dart'
;
import
'package:get/get.dart'
;
import
'package:get/get_core/src/get_main.dart'
;
import
'package:mypoint_flutter_app/screen/home/custom_widget/scrollable_header.dart'
;
import
'../../../shared/router_gage.dart'
;
import
'../../voucher/sub_widget/voucher_section_title.dart'
;
import
'../home_tab_viewmodel.dart'
;
import
'../models/achievement_model.dart'
;
import
'../models/main_service_model.dart'
;
import
'achievement_carousel_widget.dart'
;
import
'banner_carousel_widget.dart'
;
import
'main_service_grid_widget.dart'
;
class
HomeScreenWithHeader
extends
StatefulWidget
{
const
HomeScreenWithHeader
({
super
.
key
});
@override
State
<
HomeScreenWithHeader
>
createState
()
=>
_HomeScreenWithHeaderState
();
}
class
_HomeScreenWithHeaderState
extends
State
<
HomeScreenWithHeader
>
{
String
backgroundImage
=
'https://images.unsplash.com/photo-1557683316-973673baf926?w=800'
;
final
HomeTabViewModel
_viewModel
=
Get
.
put
(
HomeTabViewModel
());
bool
_showHover
=
true
;
List
<
MainServiceModel
>
_services
=
[];
List
<
AchievementModel
>
_achievements
=
[];
@override
void
initState
()
{
super
.
initState
();
loadMainServicesFromAsset
().
then
((
list
)
{
setState
(()
=>
_services
=
list
);
});
loadMainAchievementsFromAsset
().
then
((
list
)
{
setState
(()
=>
_achievements
=
list
);
});
}
// Sample data
final
String
userName
=
'Khánh'
;
final
int
coinCount
=
100
;
final
int
messageCount
=
8
;
final
String
userRank
=
'Hạng Đồng'
;
void
_changeBackground
()
{
final
List
<
String
>
backgrounds
=
[
'https://images.unsplash.com/photo-1557683316-973673baf926?w=800'
,
'https://images.unsplash.com/photo-1579952363873-27d3bfad9c0d?w=800'
,
'https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=800'
,
'https://images.unsplash.com/photo-1441974231531-c6227db76b6e?w=800'
,
'https://images.unsplash.com/photo-1469474968028-56623f02e42e?w=800'
,
];
setState
(()
{
backgroundImage
=
backgrounds
[(
backgrounds
.
indexOf
(
backgroundImage
)
+
1
)
%
backgrounds
.
length
];
});
}
@override
Widget
build
(
BuildContext
context
)
{
return
Scaffold
(
body:
SafeArea
(
top:
false
,
// Cho phép content hiển thị dưới status bar
child:
CustomScrollView
(
physics:
BouncingScrollPhysics
(),
// Hiệu ứng bounce khi scroll
slivers:
[
// Scrollable Header
ScrollableHeader
(
backgroundImageUrl:
backgroundImage
,
userName:
userName
,
coinCount:
coinCount
,
messageCount:
messageCount
,
userRank:
userRank
,
onSearchTap:
()
{
ScaffoldMessenger
.
of
(
context
).
showSnackBar
(
SnackBar
(
content:
Text
(
'Search tapped'
)));
},
onNotificationTap:
()
{
ScaffoldMessenger
.
of
(
context
).
showSnackBar
(
SnackBar
(
content:
Text
(
'Notification tapped'
)));
},
onCoinTap:
()
{
ScaffoldMessenger
.
of
(
context
).
showSnackBar
(
SnackBar
(
content:
Text
(
'Coin:
$coinCount
'
)));
},
onMessageTap:
()
{
ScaffoldMessenger
.
of
(
context
).
showSnackBar
(
SnackBar
(
content:
Text
(
'Messages:
$messageCount
'
)));
},
onRankTap:
()
{
ScaffoldMessenger
.
of
(
context
).
showSnackBar
(
SnackBar
(
content:
Text
(
'Rank:
$userRank
'
)));
},
),
// Content area
SliverToBoxAdapter
(
child:
Container
(
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'
,
],
),
if
(
_services
.
isNotEmpty
)
MainServiceGrid
(
services:
_services
,
onTap:
(
item
)
{
print
(
"Tapped:
${item.serviceName}
"
);
},
),
if
(
_achievements
.
isNotEmpty
)
HeaderSectionTitle
(
title:
'Sự kiện MyPoint'
,
onViewAll:
()
{
Get
.
toNamed
(
vouchersScreen
,
arguments:
{
"isHotProduct"
:
true
});
},
),
if
(
_achievements
.
isNotEmpty
)
AchievementCarousel
(
items:
_achievements
,
onTap:
(
item
)
{
// xử lý khi nhấn vào card
},
),
// Sample content
...
List
.
generate
(
20
,
(
index
)
=>
_buildContentCard
(
index
)),
// Bottom padding
SizedBox
(
height:
20
),
],
),
),
),
],
),
),
);
}
Widget
_buildContentCard
(
int
index
)
{
return
Container
(
margin:
EdgeInsets
.
symmetric
(
horizontal:
16
,
vertical:
8
),
padding:
EdgeInsets
.
all
(
16
),
decoration:
BoxDecoration
(
color:
Colors
.
white
,
borderRadius:
BorderRadius
.
circular
(
12
),
boxShadow:
[
BoxShadow
(
color:
Colors
.
black
.
withOpacity
(
0.1
),
blurRadius:
8
,
offset:
Offset
(
0
,
2
))],
),
child:
Row
(
children:
[
Container
(
width:
50
,
height:
50
,
decoration:
BoxDecoration
(
color:
Colors
.
red
[
400
]!.
withOpacity
(
0.1
),
borderRadius:
BorderRadius
.
circular
(
8
)),
child:
Icon
(
Icons
.
card_giftcard
,
color:
Colors
.
red
[
400
]),
),
SizedBox
(
width:
16
),
Expanded
(
child:
Column
(
crossAxisAlignment:
CrossAxisAlignment
.
start
,
children:
[
Text
(
'Nội dung
${index + 1}
'
,
style:
TextStyle
(
fontSize:
16
,
fontWeight:
FontWeight
.
w600
,
color:
Colors
.
black87
),
),
SizedBox
(
height:
4
),
Text
(
'Mô tả chi tiết cho nội dung số
${index + 1}
'
,
style:
TextStyle
(
fontSize:
14
,
color:
Colors
.
grey
[
600
]),
),
],
),
),
Icon
(
Icons
.
arrow_forward_ios
,
color:
Colors
.
grey
[
400
],
size:
16
),
],
),
);
}
Future
<
List
<
MainServiceModel
>>
loadMainServicesFromAsset
()
async
{
final
jsonStr
=
await
rootBundle
.
loadString
(
'assets/data/main_services.json'
);
final
json
=
jsonDecode
(
jsonStr
);
final
List
list
=
json
[
'data'
];
return
list
.
map
((
e
)
=>
MainServiceModel
.
fromJson
(
e
)).
toList
();
}
Future
<
List
<
AchievementModel
>>
loadMainAchievementsFromAsset
()
async
{
final
jsonStr
=
await
rootBundle
.
loadString
(
'assets/data/main_achievements.json'
);
final
json
=
jsonDecode
(
jsonStr
);
final
List
list
=
json
[
'data'
];
return
list
.
map
((
e
)
=>
AchievementModel
.
fromJson
(
e
)).
toList
();
}
}
lib/screen/home/custom_widget/hover_view.dart
0 → 100644
View file @
fda33894
import
'dart:async'
;
import
'package:flutter/material.dart'
;
class
HoverView
extends
StatefulWidget
{
final
String
imagePath
;
final
VoidCallback
?
onTap
;
final
VoidCallback
?
onClose
;
final
double
size
;
final
Color
backgroundColor
;
final
Color
closeButtonColor
;
final
double
countDownTime
;
const
HoverView
({
super
.
key
,
required
this
.
imagePath
,
this
.
onTap
,
this
.
onClose
,
this
.
size
=
100.0
,
this
.
backgroundColor
=
Colors
.
white
,
this
.
closeButtonColor
=
Colors
.
red
,
this
.
countDownTime
=
0.0
,
});
@override
State
<
HoverView
>
createState
()
=>
_HoverViewState
();
}
class
_HoverViewState
extends
State
<
HoverView
>
{
Offset
_position
=
const
Offset
(
0
,
0
);
bool
_isDragging
=
false
;
bool
_showCloseButton
=
false
;
Size
_screenSize
=
Size
.
zero
;
bool
_isInitialized
=
false
;
late
int
_remainingSeconds
;
Timer
?
_timer
;
double
get
_expandBottom
{
if
(
_remainingSeconds
>
0
)
{
return
30.0
;
}
return
8.0
;
}
@override
void
initState
()
{
super
.
initState
();
_remainingSeconds
=
widget
.
countDownTime
.
toInt
();
if
(
_remainingSeconds
>
0
)
{
_timer
=
Timer
.
periodic
(
const
Duration
(
seconds:
1
),
(
timer
)
{
if
(!
mounted
)
return
;
setState
(()
{
_remainingSeconds
--;
if
(
_remainingSeconds
<=
0
)
{
_timer
?.
cancel
();
}
});
});
}
WidgetsBinding
.
instance
.
addPostFrameCallback
((
_
)
{
_setInitialPosition
();
});
}
@override
void
dispose
()
{
_timer
?.
cancel
();
super
.
dispose
();
}
void
_setInitialPosition
()
{
if
(!
mounted
)
return
;
final
paddingBottom
=
MediaQuery
.
of
(
context
).
padding
.
bottom
+
_expandBottom
;
final
size
=
MediaQuery
.
of
(
context
).
size
;
if
(
size
.
width
>
0
&&
size
.
height
>
0
)
{
setState
(()
{
_screenSize
=
size
;
_position
=
Offset
(
_screenSize
.
width
-
widget
.
size
-
20
,
_screenSize
.
height
-
widget
.
size
-
paddingBottom
);
_isInitialized
=
true
;
});
}
}
void
_onPanStart
(
DragStartDetails
details
)
{
setState
(()
{
_isDragging
=
true
;
_showCloseButton
=
true
;
});
}
String
_formatTime
(
int
seconds
)
{
if
(
seconds
>
30
*
3600
)
{
final
days
=
(
seconds
/
86400
).
ceil
();
return
'
$days
Ngày'
;
}
else
{
final
h
=
(
seconds
~/
3600
).
toString
().
padLeft
(
2
,
'0'
);
final
m
=
((
seconds
%
3600
)
~/
60
).
toString
().
padLeft
(
2
,
'0'
);
final
s
=
(
seconds
%
60
).
toString
().
padLeft
(
2
,
'0'
);
return
'
$h
:
$m
:
$s
'
;
}
}
void
_onPanUpdate
(
DragUpdateDetails
details
)
{
if
(!
mounted
||
_screenSize
.
width
<=
0
||
_screenSize
.
height
<=
0
)
return
;
setState
(()
{
_position
+=
details
.
delta
;
// Constrain position within screen bounds
_position
=
Offset
(
_position
.
dx
.
clamp
(
0
,
_screenSize
.
width
-
widget
.
size
),
_position
.
dy
.
clamp
(
0
,
_screenSize
.
height
-
widget
.
size
),
);
});
}
void
_onPanEnd
(
DragEndDetails
details
)
{
setState
(()
{
_isDragging
=
false
;
});
_snapToNearestCorner
();
}
void
_snapToNearestCorner
()
{
final
paddingBottom
=
MediaQuery
.
of
(
context
).
padding
.
bottom
+
_expandBottom
;
if
(!
mounted
||
_screenSize
.
width
<=
0
||
_screenSize
.
height
<=
0
)
return
;
final
corners
=
[
Offset
(
20
,
paddingBottom
),
// Top left
Offset
(
_screenSize
.
width
-
widget
.
size
-
20
,
paddingBottom
),
// Top right
Offset
(
20
,
_screenSize
.
height
-
widget
.
size
-
paddingBottom
),
// Bottom left
Offset
(
_screenSize
.
width
-
widget
.
size
-
20
,
_screenSize
.
height
-
widget
.
size
-
paddingBottom
),
// Bottom right
];
Offset
nearestCorner
=
corners
[
0
];
double
minDistance
=
double
.
infinity
;
for
(
final
corner
in
corners
)
{
final
distance
=
(
_position
-
corner
).
distance
;
if
(
distance
<
minDistance
)
{
minDistance
=
distance
;
nearestCorner
=
corner
;
}
}
// Animate to nearest corner using setState
setState
(()
{
_position
=
nearestCorner
;
_showCloseButton
=
false
;
});
}
void
_onTap
()
{
if
(
widget
.
onTap
!=
null
)
{
widget
.
onTap
!();
}
}
void
_onClose
()
{
if
(
widget
.
onClose
!=
null
)
{
widget
.
onClose
!();
}
}
@override
Widget
build
(
BuildContext
context
)
{
// Update screen size on build to handle orientation changes
final
currentSize
=
MediaQuery
.
of
(
context
).
size
;
if
(
_screenSize
!=
currentSize
&&
currentSize
.
width
>
0
&&
currentSize
.
height
>
0
)
{
_screenSize
=
currentSize
;
// Adjust position if needed when screen size changes
if
(
_isInitialized
)
{
_position
=
Offset
(
_position
.
dx
.
clamp
(
0
,
_screenSize
.
width
-
widget
.
size
),
_position
.
dy
.
clamp
(
0
,
_screenSize
.
height
-
widget
.
size
),
);
}
else
{
_setInitialPosition
();
}
}
// Don't render until we have valid dimensions
if
(!
_isInitialized
||
_screenSize
.
width
<=
0
||
_screenSize
.
height
<=
0
)
{
return
const
SizedBox
.
shrink
();
}
return
Positioned
(
left:
_position
.
dx
,
top:
_position
.
dy
,
width:
widget
.
size
,
height:
widget
.
size
+
(
_remainingSeconds
>
0
?
24
:
0
),
child:
GestureDetector
(
onPanStart:
_onPanStart
,
onPanUpdate:
_onPanUpdate
,
onPanEnd:
_onPanEnd
,
onTap:
_onTap
,
onLongPress:
()
{
setState
(()
{
_showCloseButton
=
!
_showCloseButton
;
});
},
child:
Stack
(
clipBehavior:
Clip
.
none
,
children:
[
// Main container with image (no border, no pulse effect)
Column
(
children:
[
AnimatedScale
(
scale:
_isDragging
?
1.1
:
1.0
,
duration:
const
Duration
(
milliseconds:
200
),
child:
SizedBox
(
width:
widget
.
size
,
height:
widget
.
size
,
child:
widget
.
imagePath
.
startsWith
(
'http'
)
?
Image
.
network
(
widget
.
imagePath
,
fit:
BoxFit
.
cover
)
:
Image
.
asset
(
widget
.
imagePath
,
fit:
BoxFit
.
cover
),
),
),
if
(
_remainingSeconds
>
0
)
Container
(
padding:
const
EdgeInsets
.
symmetric
(
horizontal:
6
,
vertical:
2
),
decoration:
BoxDecoration
(
color:
Colors
.
black
.
withOpacity
(
0.6
),
borderRadius:
BorderRadius
.
circular
(
8
),
),
child:
Text
(
_formatTime
(
_remainingSeconds
),
style:
const
TextStyle
(
fontSize:
12
,
color:
Colors
.
white
,
fontWeight:
FontWeight
.
w500
),
),
),
],
),
// Close button
if
(
_showCloseButton
)
Positioned
(
top:
-
8
,
right:
-
8
,
width:
24
,
height:
24
,
child:
GestureDetector
(
onTap:
_onClose
,
child:
const
Icon
(
Icons
.
close
,
color:
Colors
.
transparent
,
size:
16
),
),
),
],
),
),
);
}
}
lib/screen/home/custom_widget/main_service_grid_widget.dart
0 → 100644
View file @
fda33894
import
'package:flutter/material.dart'
;
import
'../../../widgets/image_loader.dart'
;
import
'../models/main_service_model.dart'
;
class
MainServiceGrid
extends
StatelessWidget
{
final
List
<
MainServiceModel
>
services
;
final
void
Function
(
MainServiceModel
)?
onTap
;
const
MainServiceGrid
({
super
.
key
,
required
this
.
services
,
this
.
onTap
});
@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
]),
),
),
);
}
Widget
_buildItem
(
BuildContext
context
,
MainServiceModel
item
)
{
return
InkWell
(
onTap:
()
=>
onTap
?.
call
(
item
),
child:
SizedBox
(
width:
64
,
child:
Column
(
crossAxisAlignment:
CrossAxisAlignment
.
center
,
children:
[
Stack
(
alignment:
Alignment
.
topRight
,
children:
[
Container
(
width:
64
,
height:
64
,
padding:
const
EdgeInsets
.
all
(
10
),
child:
item
.
serviceIcon
!=
null
&&
item
.
serviceIcon
!.
isNotEmpty
?
Image
.
asset
(
item
.
serviceIcon
!,
fit:
BoxFit
.
contain
)
:
loadNetworkImage
(
url:
item
.
imageUrl
,
fit:
BoxFit
.
contain
,
placeholderAsset:
'assets/images/ic_logo.png'
,
),
),
if
(
item
.
eventDescrible
!=
null
&&
item
.
eventDescrible
!.
isNotEmpty
)
Positioned
(
top:
2
,
right:
2
,
child:
Container
(
padding:
const
EdgeInsets
.
symmetric
(
horizontal:
6
,
vertical:
2
),
decoration:
BoxDecoration
(
color:
Color
(
_parseColor
(
item
.
eventColor
??
'EB3C4B'
)),
borderRadius:
BorderRadius
.
circular
(
12
),
),
child:
Text
(
item
.
eventDescrible
!,
style:
const
TextStyle
(
color:
Colors
.
white
,
fontSize:
10
,
fontWeight:
FontWeight
.
bold
),
),
),
)
],
),
Text
(
maxLines:
2
,
item
.
serviceName
??
''
,
textAlign:
TextAlign
.
center
,
style:
const
TextStyle
(
fontSize:
13
),
)
],
),
),
);
}
int
_parseColor
(
String
hexColor
)
{
hexColor
=
hexColor
.
replaceAll
(
"#"
,
""
);
if
(
hexColor
.
length
==
6
)
{
hexColor
=
"FF
$hexColor
"
;
}
return
int
.
parse
(
hexColor
,
radix:
16
);
}
}
lib/screen/home/custom_widget/news_carousel_widget.dart
0 → 100644
View file @
fda33894
import
'package:flutter/material.dart'
;
import
'package:mypoint_flutter_app/widgets/image_loader.dart'
;
import
'../../faqs/faqs_model.dart'
;
class
NewsCarouselWidget
extends
StatelessWidget
{
final
List
<
PageItemModel
>
items
;
final
void
Function
(
PageItemModel
)?
onTap
;
const
NewsCarouselWidget
({
super
.
key
,
required
this
.
items
,
this
.
onTap
});
@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
]),
),
);
}
Widget
_buildItem
(
BuildContext
context
,
PageItemModel
news
)
{
final
widthItem
=
MediaQuery
.
of
(
context
).
size
.
width
/
1.6
;
return
GestureDetector
(
onTap:
()
=>
onTap
?.
call
(
news
),
child:
Container
(
width:
widthItem
,
decoration:
BoxDecoration
(
borderRadius:
BorderRadius
.
circular
(
16
),
border:
Border
.
all
(
color:
Colors
.
grey
.
shade200
),
color:
Colors
.
white
,
boxShadow:
[
BoxShadow
(
color:
Colors
.
black
.
withOpacity
(
0.05
),
blurRadius:
6
,
offset:
const
Offset
(
0
,
2
))],
),
child:
Column
(
crossAxisAlignment:
CrossAxisAlignment
.
start
,
children:
[
ClipRRect
(
borderRadius:
const
BorderRadius
.
only
(
topLeft:
Radius
.
circular
(
16
),
topRight:
Radius
.
circular
(
16
)),
child:
loadNetworkImage
(
url:
news
.
thumbnail
??
''
,
height:
widthItem
*
9
/
16
,
width:
double
.
infinity
,
fit:
BoxFit
.
cover
,
placeholderAsset:
'assets/images/bg_default_169.png'
,
),
),
Padding
(
padding:
const
EdgeInsets
.
all
(
12
),
child:
Text
(
news
.
title
??
''
,
maxLines:
2
,
overflow:
TextOverflow
.
ellipsis
,
style:
const
TextStyle
(
fontSize:
14
,
fontWeight:
FontWeight
.
bold
,
color:
Colors
.
black87
),
),
),
],
),
),
);
}
}
lib/screen/home/custom_widget/product_grid_widget.dart
0 → 100644
View file @
fda33894
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'
;
class
ProductGrid
extends
StatelessWidget
{
final
List
<
ProductModel
>
products
;
final
void
Function
(
ProductModel
)?
onTap
;
final
double
_spacing
=
12
;
const
ProductGrid
({
super
.
key
,
required
this
.
products
,
this
.
onTap
});
@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
]),
);
}
Widget
_buildItem
(
BuildContext
context
,
ProductModel
product
)
{
final
width
=
MediaQuery
.
of
(
context
).
size
.
width
;
final
widthItem
=
(
width
-
_spacing
*
3
)/
2
;
return
InkWell
(
onTap:
()
=>
onTap
?.
call
(
product
),
borderRadius:
BorderRadius
.
circular
(
12
),
child:
Container
(
decoration:
BoxDecoration
(
borderRadius:
BorderRadius
.
circular
(
12
),
border:
Border
.
all
(
color:
Colors
.
grey
.
shade200
),
),
child:
Column
(
crossAxisAlignment:
CrossAxisAlignment
.
start
,
children:
[
ClipRRect
(
borderRadius:
const
BorderRadius
.
vertical
(
top:
Radius
.
circular
(
12
)),
child:
loadNetworkImage
(
url:
product
.
banner
?.
url
??
''
,
height:
widthItem
*
9
/
16
,
width:
double
.
infinity
,
fit:
BoxFit
.
cover
,
placeholderAsset:
"assets/images/bg_default_169.png"
,
),
),
Padding
(
padding:
const
EdgeInsets
.
all
(
8
),
child:
Text
(
product
.
name
??
''
,
maxLines:
2
,
overflow:
TextOverflow
.
ellipsis
,
style:
const
TextStyle
(
fontSize:
14
,
fontWeight:
FontWeight
.
w500
),
),
),
const
Spacer
(),
Padding
(
padding:
const
EdgeInsets
.
only
(
left:
8
,
right:
8
,
bottom:
8
),
child:
CustomPointText
(
point:
product
.
amountToBePaid
??
0
,
type:
product
.
price
?.
method
),
)
],
),
),
);
}
}
lib/screen/home/custom_widget/scrollable_header.dart
0 → 100644
View file @
fda33894
import
'package:flutter/material.dart'
;
class
ScrollableHeader
extends
StatelessWidget
{
final
String
backgroundImageUrl
;
final
String
userName
;
final
int
coinCount
;
final
int
messageCount
;
final
String
userRank
;
final
VoidCallback
?
onSearchTap
;
final
VoidCallback
?
onNotificationTap
;
final
VoidCallback
?
onCoinTap
;
final
VoidCallback
?
onMessageTap
;
final
VoidCallback
?
onRankTap
;
const
ScrollableHeader
({
super
.
key
,
required
this
.
backgroundImageUrl
,
required
this
.
userName
,
this
.
coinCount
=
0
,
this
.
messageCount
=
0
,
this
.
userRank
=
'Hạng Đồng'
,
this
.
onSearchTap
,
this
.
onNotificationTap
,
this
.
onCoinTap
,
this
.
onMessageTap
,
this
.
onRankTap
,
});
@override
Widget
build
(
BuildContext
context
)
{
return
SliverAppBar
(
expandedHeight:
200.0
,
floating:
false
,
pinned:
false
,
elevation:
0
,
backgroundColor:
Colors
.
transparent
,
flexibleSpace:
FlexibleSpaceBar
(
background:
Stack
(
children:
[
// Background Image
Container
(
width:
double
.
infinity
,
height:
double
.
infinity
,
decoration:
BoxDecoration
(
image:
DecorationImage
(
image:
backgroundImageUrl
.
startsWith
(
'http'
)
?
NetworkImage
(
backgroundImageUrl
)
:
AssetImage
(
backgroundImageUrl
)
as
ImageProvider
,
fit:
BoxFit
.
cover
,
),
),
),
// Gradient overlay
Container
(
width:
double
.
infinity
,
height:
double
.
infinity
,
decoration:
BoxDecoration
(
gradient:
LinearGradient
(
begin:
Alignment
.
topCenter
,
end:
Alignment
.
bottomCenter
,
colors:
[
Colors
.
black
.
withOpacity
(
0.3
),
Colors
.
transparent
,
],
),
),
),
// MyPoint Logo
Positioned
(
top:
60
,
left:
20
,
child:
Row
(
children:
[
Text
(
'mypoint'
,
style:
TextStyle
(
color:
Colors
.
white
,
fontSize:
24
,
fontWeight:
FontWeight
.
bold
,
),
),
],
),
),
// Decorative circles
Positioned
(
top:
40
,
right:
50
,
child:
Container
(
width:
80
,
height:
80
,
decoration:
BoxDecoration
(
shape:
BoxShape
.
circle
,
color:
Colors
.
white
.
withOpacity
(
0.1
),
),
),
),
Positioned
(
top:
80
,
right:
20
,
child:
Container
(
width:
60
,
height:
60
,
decoration:
BoxDecoration
(
shape:
BoxShape
.
circle
,
color:
Colors
.
white
.
withOpacity
(
0.15
),
),
),
),
],
),
),
bottom:
PreferredSize
(
preferredSize:
Size
.
fromHeight
(
80
),
child:
Container
(
width:
double
.
infinity
,
decoration:
BoxDecoration
(
color:
Colors
.
white
,
borderRadius:
BorderRadius
.
only
(
topLeft:
Radius
.
circular
(
20
),
topRight:
Radius
.
circular
(
20
),
),
),
child:
Padding
(
padding:
EdgeInsets
.
all
(
16
),
child:
Column
(
children:
[
// Greeting and actions row
Row
(
mainAxisAlignment:
MainAxisAlignment
.
spaceBetween
,
children:
[
Text
(
'Xin chào
$userName
!'
,
style:
TextStyle
(
fontSize:
18
,
fontWeight:
FontWeight
.
w600
,
color:
Colors
.
black87
,
),
),
Row
(
children:
[
GestureDetector
(
onTap:
onSearchTap
,
child:
Container
(
padding:
EdgeInsets
.
all
(
8
),
decoration:
BoxDecoration
(
color:
Colors
.
grey
[
100
],
shape:
BoxShape
.
circle
,
),
child:
Icon
(
Icons
.
search
,
color:
Colors
.
grey
[
600
],
size:
20
,
),
),
),
SizedBox
(
width:
12
),
GestureDetector
(
onTap:
onNotificationTap
,
child:
Container
(
padding:
EdgeInsets
.
all
(
8
),
decoration:
BoxDecoration
(
color:
Colors
.
grey
[
100
],
shape:
BoxShape
.
circle
,
),
child:
Stack
(
children:
[
Icon
(
Icons
.
notifications_outlined
,
color:
Colors
.
grey
[
600
],
size:
20
,
),
if
(
messageCount
>
0
)
Positioned
(
right:
0
,
top:
0
,
child:
Container
(
padding:
EdgeInsets
.
all
(
2
),
decoration:
BoxDecoration
(
color:
Colors
.
red
,
shape:
BoxShape
.
circle
,
),
constraints:
BoxConstraints
(
minWidth:
12
,
minHeight:
12
,
),
child:
Text
(
messageCount
>
99
?
'99+'
:
messageCount
.
toString
(),
style:
TextStyle
(
color:
Colors
.
white
,
fontSize:
8
,
fontWeight:
FontWeight
.
bold
,
),
textAlign:
TextAlign
.
center
,
),
),
),
],
),
),
),
],
),
],
),
SizedBox
(
height:
16
),
// Stats row
Row
(
children:
[
_buildStatItem
(
icon:
Icons
.
monetization_on_outlined
,
value:
coinCount
.
toString
(),
iconColor:
Colors
.
orange
,
onTap:
onCoinTap
,
),
SizedBox
(
width:
12
),
_buildStatItem
(
icon:
Icons
.
mail_outline
,
value:
messageCount
.
toString
(),
iconColor:
Colors
.
blue
,
onTap:
onMessageTap
,
),
SizedBox
(
width:
12
),
_buildStatItem
(
icon:
Icons
.
person_outline
,
value:
userRank
,
iconColor:
Colors
.
green
,
onTap:
onRankTap
,
),
],
),
],
),
),
),
),
);
}
Widget
_buildStatItem
({
required
IconData
icon
,
required
String
value
,
required
Color
iconColor
,
VoidCallback
?
onTap
,
})
{
return
GestureDetector
(
onTap:
onTap
,
child:
Container
(
padding:
EdgeInsets
.
symmetric
(
horizontal:
12
,
vertical:
8
),
decoration:
BoxDecoration
(
color:
Colors
.
grey
[
50
],
borderRadius:
BorderRadius
.
circular
(
20
),
border:
Border
.
all
(
color:
Colors
.
grey
[
200
]!),
),
child:
Row
(
mainAxisSize:
MainAxisSize
.
min
,
children:
[
Container
(
padding:
EdgeInsets
.
all
(
4
),
decoration:
BoxDecoration
(
color:
iconColor
.
withOpacity
(
0.1
),
shape:
BoxShape
.
circle
,
),
child:
Icon
(
icon
,
color:
iconColor
,
size:
16
,
),
),
SizedBox
(
width:
8
),
Text
(
value
,
style:
TextStyle
(
fontSize:
14
,
fontWeight:
FontWeight
.
w500
,
color:
Colors
.
black87
,
),
),
],
),
),
);
}
}
\ No newline at end of file
lib/screen/home/home_screen.dart
View file @
fda33894
// home_screen.dart
import
'dart:convert'
;
import
'package:flutter/material.dart'
;
import
'package:flutter/services.dart'
;
import
'package:game_miniapp/game_miniapp.dart'
;
import
'package:get/get.dart'
;
import
'package:mypoint_flutter_app/preference/data_preference.dart'
;
import
'package:mypoint_flutter_app/screen/home/custom_widget/header_home.dart'
;
import
'package:mypoint_flutter_app/screen/home/custom_widget/product_grid_widget.dart'
;
import
'package:mypoint_flutter_app/screen/home/pipi_detail_screen.dart'
;
import
'package:mypoint_flutter_app/shared/router_gage.dart'
;
import
'../setting/setting_screen.dart'
;
import
'../voucher/sub_widget/voucher_section_title.dart'
;
import
'custom_widget/achievement_carousel_widget.dart'
;
import
'custom_widget/banner_carousel_widget.dart'
;
import
'custom_widget/hover_view.dart'
;
import
'custom_widget/main_service_grid_widget.dart'
;
import
'custom_widget/news_carousel_widget.dart'
;
import
'home_tab_viewmodel.dart'
;
import
'models/achievement_model.dart'
;
import
'models/main_service_model.dart'
;
class
HomeScreen
extends
State
less
Widget
{
class
HomeScreen
extends
State
ful
Widget
{
const
HomeScreen
({
super
.
key
});
@override
State
<
HomeScreen
>
createState
()
=>
_HomeScreenState
();
}
class
_HomeScreenState
extends
State
<
HomeScreen
>
{
final
HomeTabViewModel
_viewModel
=
Get
.
put
(
HomeTabViewModel
());
bool
_showHover
=
true
;
@override
void
initState
()
{
super
.
initState
();
_viewModel
.
getSectionLayoutHome
();
}
Widget
_buildSliverHeader
(
double
heightHeader
)
{
return
Obx
(()
{
final
data
=
_viewModel
.
headerHomeData
.
value
;
if
(
data
==
null
)
return
SliverToBoxAdapter
(
child:
SizedBox
.
shrink
());
return
SliverToBoxAdapter
(
child:
HomeGreetingHeader
(
dataHeader:
data
,
heightContent:
heightHeader
,
),
);
});
}
@override
Widget
build
(
BuildContext
context
)
{
final
paddingBottom
=
MediaQuery
.
of
(
context
).
padding
.
bottom
+
20
;
final
width
=
MediaQuery
.
of
(
context
).
size
.
width
;
final
heightHeader
=
width
*
86
/
375
+
112
;
return
Scaffold
(
body:
Center
(
child:
Column
(
mainAxisAlignment:
MainAxisAlignment
.
center
,
crossAxisAlignment:
CrossAxisAlignment
.
center
,
body:
Stack
(
children:
[
NestedScrollView
(
physics:
AlwaysScrollableScrollPhysics
(),
headerSliverBuilder:
(
_
,
_
)
=>
[
_buildSliverHeader
(
heightHeader
),
// SliverToBoxAdapter(
// child: Obx(() {
// if (_viewModel.headerHomeData.value == null) return SizedBox.shrink();
// return HomeGreetingHeader(
// dataHeader: _viewModel.headerHomeData.value!,
// heightContent: heightHeader);
// }),
// ),
],
body:
RefreshIndicator
(
onRefresh:
_onRefresh
,
child:
Obx
(()
{
return
ListView
(
padding:
EdgeInsets
.
only
(
bottom:
paddingBottom
),
physics:
AlwaysScrollableScrollPhysics
(),
children:
[
ElevatedButton
(
onPressed:
()
=>
_showMiniGame
(
context
),
child:
const
Text
(
'Mini Game'
)),
ElevatedButton
(
onPressed:
()
=>
_logout
(
context
),
child:
const
Text
(
'Đăng xuất'
)),
ElevatedButton
(
onPressed:
()
=>
_showSetting
(
context
),
child:
const
Text
(
'Setting'
)),
ElevatedButton
(
onPressed:
()
=>
_showNotify
(
context
),
child:
const
Text
(
'Notify'
)),
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'
,
],
),
if
(
_viewModel
.
services
.
value
.
isNotEmpty
)
MainServiceGrid
(
services:
_viewModel
.
services
.
value
,
onTap:
(
item
)
{
item
.
directionalScreen
?.
begin
();
},
),
if
(
_viewModel
.
achievements
.
value
.
isNotEmpty
)
HeaderSectionTitle
(
title:
'Sự kiện MyPoint'
,
onViewAll:
()
{
Get
.
toNamed
(
achievementListScreen
);
},
),
if
(
_viewModel
.
achievements
.
value
.
isNotEmpty
)
AchievementCarousel
(
items:
_viewModel
.
achievements
.
value
,
onTap:
(
item
)
{
item
.
directionScreen
?.
begin
();
},
),
if
(
_viewModel
.
products
.
value
.
isNotEmpty
)
ProductGrid
(
products:
_viewModel
.
products
.
value
,
onTap:
(
product
)
{
Get
.
toNamed
(
voucherDetailScreen
,
arguments:
product
.
id
);
},
),
if
(
_viewModel
.
news
.
value
.
isNotEmpty
)
HeaderSectionTitle
(
title:
'MyPoint có gì hot?'
,
onViewAll:
()
{
Get
.
toNamed
(
newsListScreen
);
},
),
if
(
_viewModel
.
news
.
value
.
isNotEmpty
)
NewsCarouselWidget
(
items:
_viewModel
.
news
.
value
,
onTap:
(
item
)
async
{
Get
.
toNamed
(
campaignDetailScreen
,
arguments:
{
"id"
:
item
.
pageId
});
},
),
// ElevatedButton(onPressed: () => _showMiniGame(context), child: const Text('Mini Game')),
// ElevatedButton(onPressed: () => _logout(context), child: const Text('Đăng xuất')),
// ElevatedButton(onPressed: () => _showSetting(context), child: const Text('Setting')),
// ElevatedButton(onPressed: () => _showNotify(context), child: const Text('Notify')),
],
);
}),
),
),
if
(
_showHover
)
Positioned
.
fill
(
child:
Obx
(()
{
return
HoverView
(
imagePath:
_viewModel
.
hoverData
.
value
?.
icon
??
''
,
onTap:
_handleHoverViewTap
,
onClose:
_handleCloseHoverView
,
backgroundColor:
Colors
.
transparent
,
size:
80
,
countDownTime:
_viewModel
.
hoverData
.
value
?.
countDownTime
??
0.0
,
);
}),
),
],
),
);
}
void
_showNotify
(
BuildContext
context
)
async
{
Get
.
toNamed
(
notificationScreen
);
void
_handleHoverViewTap
()
{
showModalBottomSheet
(
context:
context
,
backgroundColor:
Colors
.
transparent
,
isScrollControlled:
true
,
builder:
(
_
)
=>
PipiDetailScreen
(),
);
}
void
_handleCloseHoverView
()
{
setState
(()
{
_showHover
=
false
;
});
}
Future
<
void
>
_onRefresh
()
async
{
print
(
"onRefresh"
);
await
_viewModel
.
getSectionLayoutHome
();
}
void
_showMiniGame
(
BuildContext
context
)
async
{
Navigator
.
push
(
context
,
MaterialPageRoute
(
builder:
(
_
)
=>
const
GameMiniAppScreen
()),
);
Navigator
.
push
(
context
,
MaterialPageRoute
(
builder:
(
_
)
=>
const
GameMiniAppScreen
()));
}
void
_logout
(
BuildContext
context
)
async
{
...
...
@@ -51,11 +195,9 @@ class HomeScreen extends StatelessWidget {
],
),
);
if
(
confirm
==
true
)
{
DataPreference
.
instance
.
clearLoginToken
();
_safeBackToLogin
();
// Get.until((route) => route.settings.name == loginScreen);
}
}
...
...
lib/screen/home/home_tab_viewmodel.dart
0 → 100644
View file @
fda33894
import
'dart:convert'
;
import
'package:flutter/services.dart'
;
import
'package:get/get_rx/src/rx_types/rx_types.dart'
;
import
'package:mypoint_flutter_app/networking/restful_api_request.dart'
;
import
'../../base/restful_api_viewmodel.dart'
;
import
'../../preference/point/header_home_model.dart'
;
import
'../faqs/faqs_model.dart'
;
import
'../voucher/models/product_model.dart'
;
import
'../voucher/models/product_type.dart'
;
import
'models/achievement_model.dart'
;
import
'models/hover_data_model.dart'
;
import
'models/main_section_config_model.dart'
;
import
'models/main_service_model.dart'
;
import
'models/notification_unread_model.dart'
;
class
HomeTabViewModel
extends
RestfulApiViewModel
{
final
RxList
<
ProductModel
>
products
=
<
ProductModel
>[].
obs
;
final
RxList
<
PageItemModel
>
news
=
<
PageItemModel
>[].
obs
;
final
RxList
<
MainServiceModel
>
services
=
<
MainServiceModel
>[].
obs
;
final
RxList
<
AchievementModel
>
achievements
=
<
AchievementModel
>[].
obs
;
var
hoverData
=
Rxn
<
HoverDataModel
>();
var
notificationUnreadData
=
Rxn
<
NotificationUnreadData
>();
var
headerHomeData
=
Rxn
<
HeaderHomeModel
>();
List
<
MainSectionConfigModel
>
sectionLayouts
=
[];
@override
void
onInit
()
{
super
.
onInit
();
getDynamicHeaderHome
();
// getSectionLayoutHome();
// getHotProducts();
// fetchFAQItems();
// loadMainServicesFromAsset();
// loadMainAchievementsFromAsset();
// loadDataPiPiHome();
// getNotificationUnread();
}
Future
<
void
>
getSectionLayoutHome
()
async
{
showLoading
();
try
{
final
response
=
await
client
.
getSectionLayoutHome
();
if
(
response
.
data
!=
null
)
{
sectionLayouts
=
response
.
data
??
[];
}
}
catch
(
error
)
{
print
(
"Error fetching section layout:
$error
"
);
}
finally
{
hideLoading
();
}
}
Future
<
void
>
getHotProducts
()
async
{
final
body
=
{
"type"
:
ProductType
.
voucher
.
value
,
"size"
:
10
,
"index"
:
0
,
"catalog_code"
:
"HOT"
,
};
try
{
final
result
=
await
client
.
getProducts
(
body
);
products
.
value
=
result
.
data
??
[];
}
catch
(
error
)
{
print
(
"Error fetching hot products:
$error
"
);
}
}
Future
<
void
>
loadDataPiPiHome
()
async
{
try
{
final
result
=
await
client
.
getDataPiPiHome
();
hoverData
.
value
=
result
.
data
;
}
catch
(
error
)
{
print
(
"Error fetching loadDataPiPiHome:
$error
"
);
}
}
Future
<
void
>
getDynamicHeaderHome
()
async
{
try
{
final
result
=
await
client
.
getDynamicHeaderHome
();
headerHomeData
.
value
=
result
.
data
;
}
catch
(
error
)
{
print
(
"Error fetching getDynamicHeaderHome:
$error
"
);
}
}
Future
<
void
>
getNotificationUnread
()
async
{
try
{
final
result
=
await
client
.
getNotificationUnread
();
notificationUnreadData
.
value
=
result
.
data
;
}
catch
(
error
)
{
print
(
"Error fetching hot products:
$error
"
);
}
}
Future
<
void
>
fetchFAQItems
()
async
{
showLoading
();
client
.
websiteFolderGetPageList
({
"folder_uri"
:
"TIN-TUC"
,
"limit"
:
20
}).
then
((
value
)
{
hideLoading
();
news
.
value
=
value
.
data
?.
items
??
[];
});
}
Future
<
void
>
loadMainServicesFromAsset
()
async
{
final
jsonStr
=
await
rootBundle
.
loadString
(
'assets/data/main_services.json'
);
final
json
=
jsonDecode
(
jsonStr
);
final
List
list
=
json
[
'data'
];
services
.
value
=
list
.
map
((
e
)
=>
MainServiceModel
.
fromJson
(
e
)).
toList
();
}
Future
<
void
>
loadMainAchievementsFromAsset
()
async
{
final
jsonStr
=
await
rootBundle
.
loadString
(
'assets/data/main_achievements.json'
);
final
json
=
jsonDecode
(
jsonStr
);
final
List
list
=
json
[
'data'
];
achievements
.
value
=
list
.
map
((
e
)
=>
AchievementModel
.
fromJson
(
e
)).
toList
();
}
}
\ No newline at end of file
lib/screen/home/models/achievement_model.dart
0 → 100644
View file @
fda33894
import
'package:get/get.dart'
;
import
'package:json_annotation/json_annotation.dart'
;
import
'package:mypoint_flutter_app/directional/directional_screen.dart'
;
part
'achievement_model.g.dart'
;
@JsonSerializable
()
class
AchievementModel
{
final
String
?
id
;
@JsonKey
(
name:
'achievement_name'
)
final
String
?
achievementName
;
@JsonKey
(
name:
'achievement_icon_url'
)
final
String
?
achievementIconUrl
;
@JsonKey
(
name:
'apply_for_group'
)
final
String
?
applyForGroup
;
@JsonKey
(
name:
'click_action_type'
)
final
String
?
clickActionType
;
@JsonKey
(
name:
'click_action_param'
)
final
String
?
clickActionParam
;
final
List
<
AchievementImageModel
>?
images
;
const
AchievementModel
({
this
.
id
,
this
.
achievementName
,
this
.
achievementIconUrl
,
this
.
applyForGroup
,
this
.
clickActionType
,
this
.
clickActionParam
,
this
.
images
,
});
DirectionalScreen
?
get
directionScreen
{
return
DirectionalScreen
.
build
(
clickActionType:
clickActionType
,
clickActionParam:
clickActionParam
,
);
}
String
?
get
urlBackground
{
final
coverImage
=
images
?.
firstWhereOrNull
(
(
image
)
=>
image
.
imageType
==
'COVER'
,
);
final
url
=
coverImage
?.
imageUrl
??
images
?.
firstOrNull
?.
imageUrl
;
print
(
"urlBackground:
$url
"
);
return
url
;
}
factory
AchievementModel
.
fromJson
(
Map
<
String
,
dynamic
>
json
)
=>
_$AchievementModelFromJson
(
json
);
Map
<
String
,
dynamic
>
toJson
()
=>
_$AchievementModelToJson
(
this
);
}
@JsonSerializable
()
class
AchievementImageModel
{
final
String
?
id
;
@JsonKey
(
name:
'image_type'
)
final
String
?
imageType
;
@JsonKey
(
name:
'image_url'
)
final
String
?
imageUrl
;
const
AchievementImageModel
({
this
.
id
,
this
.
imageType
,
this
.
imageUrl
,
});
factory
AchievementImageModel
.
fromJson
(
Map
<
String
,
dynamic
>
json
)
=>
_$AchievementImageModelFromJson
(
json
);
Map
<
String
,
dynamic
>
toJson
()
=>
_$AchievementImageModelToJson
(
this
);
}
class
AchievementListResponse
{
final
List
<
AchievementModel
>?
achievements
;
const
AchievementListResponse
({
this
.
achievements
});
factory
AchievementListResponse
.
fromJson
(
Map
<
String
,
dynamic
>
json
)
{
return
AchievementListResponse
(
achievements:
(
json
[
'achievements'
]
as
List
?)
?.
map
((
e
)
=>
AchievementModel
.
fromJson
(
e
as
Map
<
String
,
dynamic
>))
.
toList
(),
);
}
Map
<
String
,
dynamic
>
toJson
()
{
return
{
'achievements'
:
achievements
?.
map
((
e
)
=>
e
.
toJson
()).
toList
(),
};
}
}
\ No newline at end of file
lib/screen/home/models/achievement_model.g.dart
0 → 100644
View file @
fda33894
// GENERATED CODE - DO NOT MODIFY BY HAND
part of
'achievement_model.dart'
;
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
AchievementModel
_$AchievementModelFromJson
(
Map
<
String
,
dynamic
>
json
)
=>
AchievementModel
(
id:
json
[
'id'
]
as
String
?,
achievementName:
json
[
'achievement_name'
]
as
String
?,
achievementIconUrl:
json
[
'achievement_icon_url'
]
as
String
?,
applyForGroup:
json
[
'apply_for_group'
]
as
String
?,
clickActionType:
json
[
'click_action_type'
]
as
String
?,
clickActionParam:
json
[
'click_action_param'
]
as
String
?,
images:
(
json
[
'images'
]
as
List
<
dynamic
>?)
?.
map
(
(
e
)
=>
AchievementImageModel
.
fromJson
(
e
as
Map
<
String
,
dynamic
>),
)
.
toList
(),
);
Map
<
String
,
dynamic
>
_$AchievementModelToJson
(
AchievementModel
instance
)
=>
<
String
,
dynamic
>{
'id'
:
instance
.
id
,
'achievement_name'
:
instance
.
achievementName
,
'achievement_icon_url'
:
instance
.
achievementIconUrl
,
'apply_for_group'
:
instance
.
applyForGroup
,
'click_action_type'
:
instance
.
clickActionType
,
'click_action_param'
:
instance
.
clickActionParam
,
'images'
:
instance
.
images
,
};
AchievementImageModel
_$AchievementImageModelFromJson
(
Map
<
String
,
dynamic
>
json
,
)
=>
AchievementImageModel
(
id:
json
[
'id'
]
as
String
?,
imageType:
json
[
'image_type'
]
as
String
?,
imageUrl:
json
[
'image_url'
]
as
String
?,
);
Map
<
String
,
dynamic
>
_$AchievementImageModelToJson
(
AchievementImageModel
instance
,
)
=>
<
String
,
dynamic
>{
'id'
:
instance
.
id
,
'image_type'
:
instance
.
imageType
,
'image_url'
:
instance
.
imageUrl
,
};
lib/screen/home/models/brand_category_model.dart
0 → 100644
View file @
fda33894
import
'package:json_annotation/json_annotation.dart'
;
part
'brand_category_model.g.dart'
;
// Nếu dùng build_runner
@JsonSerializable
()
class
BrandCategoryModel
{
final
String
?
id
;
final
String
?
subscribed
;
@JsonKey
(
name:
'category_code'
)
final
String
?
categoryCode
;
@JsonKey
(
name:
'category_name'
)
final
String
?
categoryName
;
@JsonKey
(
name:
'image_url'
)
final
String
?
imageUrl
;
const
BrandCategoryModel
({
this
.
id
,
this
.
subscribed
,
this
.
categoryCode
,
this
.
categoryName
,
this
.
imageUrl
,
});
factory
BrandCategoryModel
.
fromJson
(
Map
<
String
,
dynamic
>
json
)
=>
_$BrandCategoryModelFromJson
(
json
);
Map
<
String
,
dynamic
>
toJson
()
=>
_$BrandCategoryModelToJson
(
this
);
}
lib/screen/home/models/brand_category_model.g.dart
0 → 100644
View file @
fda33894
// GENERATED CODE - DO NOT MODIFY BY HAND
part of
'brand_category_model.dart'
;
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
BrandCategoryModel
_$BrandCategoryModelFromJson
(
Map
<
String
,
dynamic
>
json
)
=>
BrandCategoryModel
(
id:
json
[
'id'
]
as
String
?,
subscribed:
json
[
'subscribed'
]
as
String
?,
categoryCode:
json
[
'category_code'
]
as
String
?,
categoryName:
json
[
'category_name'
]
as
String
?,
imageUrl:
json
[
'image_url'
]
as
String
?,
);
Map
<
String
,
dynamic
>
_$BrandCategoryModelToJson
(
BrandCategoryModel
instance
)
=>
<
String
,
dynamic
>{
'id'
:
instance
.
id
,
'subscribed'
:
instance
.
subscribed
,
'category_code'
:
instance
.
categoryCode
,
'category_name'
:
instance
.
categoryName
,
'image_url'
:
instance
.
imageUrl
,
};
Prev
1
2
3
4
5
Next
Write
Preview
Supports
Markdown
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment