feat: home page
This commit is contained in:
parent
c3eb62077f
commit
627de219cb
@ -1,4 +1,5 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'dart:math' as math;
|
||||||
|
|
||||||
import '../../../common/theme/theme.dart';
|
import '../../../common/theme/theme.dart';
|
||||||
import '../assets/assets.gen.dart';
|
import '../assets/assets.gen.dart';
|
||||||
|
|||||||
@ -1,71 +1,161 @@
|
|||||||
part of 'image.dart';
|
part of 'image.dart';
|
||||||
|
|
||||||
class ImagePlaceholder extends StatelessWidget {
|
class ImagePlaceholder extends StatelessWidget {
|
||||||
const ImagePlaceholder({super.key});
|
const ImagePlaceholder({
|
||||||
|
super.key,
|
||||||
|
this.width,
|
||||||
|
this.height,
|
||||||
|
this.showBorderRadius = true,
|
||||||
|
this.backgroundColor,
|
||||||
|
});
|
||||||
|
|
||||||
|
final double? width;
|
||||||
|
final double? height;
|
||||||
|
final bool showBorderRadius;
|
||||||
|
final Color? backgroundColor;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
return LayoutBuilder(
|
||||||
|
builder: (context, constraints) {
|
||||||
|
// Determine the size based on available space or provided dimensions
|
||||||
|
final containerWidth = width ?? constraints.maxWidth;
|
||||||
|
final containerHeight = height ?? constraints.maxHeight;
|
||||||
|
|
||||||
|
// Calculate the minimum dimension to determine if we should show simple or detailed version
|
||||||
|
final minDimension = math.min(
|
||||||
|
containerWidth == double.infinity ? containerHeight : containerWidth,
|
||||||
|
containerHeight == double.infinity ? containerWidth : containerHeight,
|
||||||
|
);
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
width: double.infinity,
|
width: containerWidth == double.infinity
|
||||||
decoration: const BoxDecoration(
|
? double.infinity
|
||||||
color: Color(0x4DD9D9D9), // Light gray with opacity
|
: containerWidth,
|
||||||
borderRadius: BorderRadius.only(
|
height: containerHeight == double.infinity ? null : containerHeight,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: backgroundColor ?? const Color(0x4DD9D9D9),
|
||||||
|
borderRadius: showBorderRadius
|
||||||
|
? const BorderRadius.only(
|
||||||
bottomLeft: Radius.circular(20),
|
bottomLeft: Radius.circular(20),
|
||||||
bottomRight: Radius.circular(20),
|
bottomRight: Radius.circular(20),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
),
|
),
|
||||||
|
child: Center(
|
||||||
|
child: minDimension < 100
|
||||||
|
? _buildSimpleVersion(minDimension)
|
||||||
|
: _buildDetailedVersion(minDimension),
|
||||||
),
|
),
|
||||||
child: Column(
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simple version for small sizes (< 100px)
|
||||||
|
Widget _buildSimpleVersion(double size) {
|
||||||
|
final iconSize = (size * 0.4).clamp(16.0, 32.0);
|
||||||
|
final fontSize = (size * 0.12).clamp(8.0, 12.0);
|
||||||
|
|
||||||
|
return Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
// Hand holding coffee illustration
|
|
||||||
Container(
|
Container(
|
||||||
width: 120,
|
width: iconSize * 1.5,
|
||||||
height: 160,
|
height: iconSize * 1.5,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
borderRadius: BorderRadius.circular(60),
|
borderRadius: BorderRadius.circular(iconSize * 0.75),
|
||||||
|
),
|
||||||
|
child: Center(
|
||||||
|
child: Assets.images.logo.image(
|
||||||
|
width: iconSize,
|
||||||
|
height: iconSize,
|
||||||
|
fit: BoxFit.contain,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (size > 50) ...[
|
||||||
|
SizedBox(height: size * 0.05),
|
||||||
|
Text(
|
||||||
|
'Enaklo',
|
||||||
|
style: TextStyle(
|
||||||
|
color: AppColor.primary,
|
||||||
|
fontSize: fontSize,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detailed version for larger sizes (>= 100px)
|
||||||
|
Widget _buildDetailedVersion(double minDimension) {
|
||||||
|
final scaleFactor = minDimension / 200; // Base scale factor
|
||||||
|
|
||||||
|
// Proportional sizes
|
||||||
|
final illustrationSize = (120 * scaleFactor).clamp(80.0, 120.0);
|
||||||
|
final illustrationHeight = (160 * scaleFactor).clamp(100.0, 160.0);
|
||||||
|
final handWidth = (60 * scaleFactor).clamp(30.0, 60.0);
|
||||||
|
final handHeight = (80 * scaleFactor).clamp(40.0, 80.0);
|
||||||
|
final cupWidth = (70 * scaleFactor).clamp(35.0, 70.0);
|
||||||
|
final cupHeight = (90 * scaleFactor).clamp(45.0, 90.0);
|
||||||
|
final logoSize = (40 * scaleFactor).clamp(20.0, 40.0);
|
||||||
|
final fontSize = (12 * scaleFactor).clamp(8.0, 12.0);
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
width: illustrationSize,
|
||||||
|
height: illustrationHeight,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(illustrationSize / 2),
|
||||||
),
|
),
|
||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
// Hand
|
// Hand
|
||||||
Positioned(
|
Positioned(
|
||||||
bottom: 20,
|
bottom: illustrationHeight * 0.125, // 20/160 ratio
|
||||||
left: 30,
|
left: illustrationSize * 0.25, // 30/120 ratio
|
||||||
child: Container(
|
child: Container(
|
||||||
width: 60,
|
width: handWidth,
|
||||||
height: 80,
|
height: handHeight,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: const Color(0xFFFFDBB3),
|
color: const Color(0xFFFFDBB3),
|
||||||
borderRadius: BorderRadius.circular(30),
|
borderRadius: BorderRadius.circular(handWidth / 2),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
// Coffee cup
|
// Coffee cup
|
||||||
Positioned(
|
Positioned(
|
||||||
top: 30,
|
top: illustrationHeight * 0.1875, // 30/160 ratio
|
||||||
left: 25,
|
left: illustrationSize * 0.208, // 25/120 ratio
|
||||||
child: Container(
|
child: Container(
|
||||||
width: 70,
|
width: cupWidth,
|
||||||
height: 90,
|
height: cupHeight,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: const Color(0xFFF4E4BC),
|
color: const Color(0xFFF4E4BC),
|
||||||
borderRadius: BorderRadius.circular(10),
|
borderRadius: BorderRadius.circular(
|
||||||
|
math.max(8.0, 10 * scaleFactor),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
// Fore logo
|
// Logo
|
||||||
Assets.images.logo.image(
|
Assets.images.logo.image(
|
||||||
width: 40,
|
width: logoSize,
|
||||||
height: 40,
|
height: logoSize,
|
||||||
fit: BoxFit.contain,
|
fit: BoxFit.contain,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
SizedBox(height: math.max(4.0, 8 * scaleFactor)),
|
||||||
|
if (cupHeight > 50) // Only show text if cup is big enough
|
||||||
Text(
|
Text(
|
||||||
'Enaklo',
|
'Enaklo',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: AppColor.primary,
|
color: AppColor.primary,
|
||||||
fontSize: 12,
|
fontSize: fontSize,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -75,9 +165,6 @@ class ImagePlaceholder extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,12 +1,171 @@
|
|||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:carousel_slider/carousel_slider.dart';
|
||||||
|
|
||||||
|
import '../../../../../common/theme/theme.dart';
|
||||||
|
import '../../../../components/image/image.dart';
|
||||||
|
import 'widgets/feature_section.dart';
|
||||||
|
import 'widgets/lottery_card.dart';
|
||||||
|
import 'widgets/point_card.dart';
|
||||||
|
import 'widgets/popular_merchant_section.dart';
|
||||||
|
|
||||||
@RoutePage()
|
@RoutePage()
|
||||||
class HomePage extends StatelessWidget {
|
class HomePage extends StatefulWidget {
|
||||||
const HomePage({super.key});
|
const HomePage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<HomePage> createState() => _HomePageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _HomePageState extends State<HomePage> {
|
||||||
|
int _currentCarouselIndex = 0;
|
||||||
|
final CarouselSliderController _carouselController =
|
||||||
|
CarouselSliderController();
|
||||||
|
|
||||||
|
final List<String> _carouselImages = [
|
||||||
|
'https://images.unsplash.com/photo-1509042239860-f550ce710b93?w=800&h=400&fit=crop',
|
||||||
|
'https://images.unsplash.com/photo-1495474472287-4d71bcdd2085?w=800&h=400&fit=crop',
|
||||||
|
'https://images.unsplash.com/photo-1461023058943-07fcbe16d735?w=800&h=400&fit=crop',
|
||||||
|
'https://images.unsplash.com/photo-1574848794584-c740d6a5595f?w=800&h=400&fit=crop',
|
||||||
|
];
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Center(child: Text('Home Page'));
|
return Scaffold(
|
||||||
|
body: SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
_buildHeaderSection(),
|
||||||
|
const SizedBox(height: 70),
|
||||||
|
HomeFeatureSection(),
|
||||||
|
HomeLotteryBanner(),
|
||||||
|
HomePopularMerchantSection(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildHeaderSection() {
|
||||||
|
return Stack(
|
||||||
|
clipBehavior: Clip.none,
|
||||||
|
children: [
|
||||||
|
_buildCarouselBanner(),
|
||||||
|
_buildNotificationButton(),
|
||||||
|
|
||||||
|
Positioned(
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
top: 225,
|
||||||
|
child: _buildCarouselIndicators(),
|
||||||
|
),
|
||||||
|
Positioned(left: 16, right: 16, top: 240, child: HomePointCard()),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notification Button
|
||||||
|
Widget _buildNotificationButton() {
|
||||||
|
return Positioned(
|
||||||
|
top: MediaQuery.of(context).padding.top + 10,
|
||||||
|
right: 16,
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppColor.black.withOpacity(0.3),
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
child: const Icon(
|
||||||
|
Icons.notifications_outlined,
|
||||||
|
color: AppColor.white,
|
||||||
|
size: 20,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
right: 8,
|
||||||
|
top: 8,
|
||||||
|
child: Container(
|
||||||
|
width: 8,
|
||||||
|
height: 8,
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
color: AppColor.primary,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Carousel Banner (Full Width)
|
||||||
|
Widget _buildCarouselBanner() {
|
||||||
|
return CarouselSlider(
|
||||||
|
carouselController: _carouselController,
|
||||||
|
options: CarouselOptions(
|
||||||
|
height: 280,
|
||||||
|
viewportFraction: 1.0, // Full width
|
||||||
|
enlargeCenterPage: false,
|
||||||
|
autoPlay: true,
|
||||||
|
autoPlayInterval: const Duration(seconds: 4),
|
||||||
|
onPageChanged: (index, reason) {
|
||||||
|
setState(() {
|
||||||
|
_currentCarouselIndex = index;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
items: _carouselImages
|
||||||
|
.skip(1)
|
||||||
|
.map((imageUrl) => _buildImageSlide(imageUrl))
|
||||||
|
.toList(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildImageSlide(String imageUrl) {
|
||||||
|
return SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: Image.network(
|
||||||
|
imageUrl,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
loadingBuilder: (context, child, loadingProgress) {
|
||||||
|
if (loadingProgress == null) return child;
|
||||||
|
return Container(
|
||||||
|
color: AppColor.textLight,
|
||||||
|
child: const Center(
|
||||||
|
child: CircularProgressIndicator(color: AppColor.primary),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
errorBuilder: (context, error, stackTrace) {
|
||||||
|
return ImagePlaceholder();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildCarouselIndicators() {
|
||||||
|
return Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: List.generate(4, (index) {
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: () => _carouselController.animateToPage(index),
|
||||||
|
child: Container(
|
||||||
|
width: _currentCarouselIndex == index ? 24 : 8,
|
||||||
|
height: 8,
|
||||||
|
margin: const EdgeInsets.symmetric(horizontal: 3),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
color: _currentCarouselIndex == index
|
||||||
|
? AppColor.primary
|
||||||
|
: AppColor.textLight,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
100
lib/presentation/pages/main/pages/home/widgets/feature_card.dart
Normal file
100
lib/presentation/pages/main/pages/home/widgets/feature_card.dart
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import '../../../../../../common/theme/theme.dart';
|
||||||
|
|
||||||
|
class HomeFeatureCard extends StatefulWidget {
|
||||||
|
final IconData icon;
|
||||||
|
final String title;
|
||||||
|
final Color iconColor;
|
||||||
|
final VoidCallback onTap;
|
||||||
|
|
||||||
|
const HomeFeatureCard({
|
||||||
|
super.key,
|
||||||
|
required this.icon,
|
||||||
|
required this.title,
|
||||||
|
required this.iconColor,
|
||||||
|
required this.onTap,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<HomeFeatureCard> createState() => _HomeFeatureCardState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _HomeFeatureCardState extends State<HomeFeatureCard>
|
||||||
|
with SingleTickerProviderStateMixin {
|
||||||
|
late AnimationController _controller;
|
||||||
|
late Animation<double> _scaleAnimation;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_controller = AnimationController(
|
||||||
|
duration: const Duration(milliseconds: 100),
|
||||||
|
vsync: this,
|
||||||
|
);
|
||||||
|
_scaleAnimation = Tween<double>(
|
||||||
|
begin: 1.0,
|
||||||
|
end: 0.95,
|
||||||
|
).animate(CurvedAnimation(parent: _controller, curve: Curves.easeInOut));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_controller.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: widget.onTap,
|
||||||
|
onTapDown: (_) => _controller.forward(),
|
||||||
|
onTapUp: (_) => _controller.reverse(),
|
||||||
|
onTapCancel: () => _controller.reverse(),
|
||||||
|
child: AnimatedBuilder(
|
||||||
|
animation: _scaleAnimation,
|
||||||
|
builder: (context, child) {
|
||||||
|
return Transform.scale(
|
||||||
|
scale: _scaleAnimation.value,
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 64,
|
||||||
|
height: 64,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppColor.white,
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: AppColor.black.withOpacity(0.08),
|
||||||
|
blurRadius: 12,
|
||||||
|
offset: const Offset(0, 4),
|
||||||
|
spreadRadius: 0,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Icon(widget.icon, color: widget.iconColor, size: 28),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
Text(
|
||||||
|
widget.title,
|
||||||
|
style: AppStyle.sm.copyWith(
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: AppColor.textPrimary,
|
||||||
|
letterSpacing: -0.2,
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,36 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'feature_card.dart';
|
||||||
|
|
||||||
|
class HomeFeatureSection extends StatelessWidget {
|
||||||
|
const HomeFeatureSection({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
|
children: [
|
||||||
|
HomeFeatureCard(
|
||||||
|
icon: Icons.card_giftcard,
|
||||||
|
title: 'Reward',
|
||||||
|
iconColor: const Color(0xFF1976D2),
|
||||||
|
onTap: () => print('Navigate to Reward'),
|
||||||
|
),
|
||||||
|
HomeFeatureCard(
|
||||||
|
icon: Icons.casino,
|
||||||
|
title: 'Undian',
|
||||||
|
iconColor: const Color(0xFF7B1FA2),
|
||||||
|
onTap: () => print('Navigate to Undian'),
|
||||||
|
),
|
||||||
|
HomeFeatureCard(
|
||||||
|
icon: Icons.store,
|
||||||
|
title: 'Merchant',
|
||||||
|
iconColor: const Color(0xFF388E3C),
|
||||||
|
onTap: () => print('Navigate to Merchant'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
396
lib/presentation/pages/main/pages/home/widgets/lottery_card.dart
Normal file
396
lib/presentation/pages/main/pages/home/widgets/lottery_card.dart
Normal file
@ -0,0 +1,396 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import '../../../../../../common/theme/theme.dart';
|
||||||
|
|
||||||
|
class HomeLotteryBanner extends StatefulWidget {
|
||||||
|
const HomeLotteryBanner({
|
||||||
|
super.key,
|
||||||
|
this.onTap,
|
||||||
|
this.title = "🎰 UNDIAN BERHADIAH",
|
||||||
|
this.subtitle = "Kumpulkan voucher untuk menang hadiah menarik!",
|
||||||
|
this.showAnimation = true,
|
||||||
|
this.actionText = "MAIN SEKARANG",
|
||||||
|
});
|
||||||
|
|
||||||
|
final VoidCallback? onTap;
|
||||||
|
final String title;
|
||||||
|
final String subtitle;
|
||||||
|
final bool showAnimation;
|
||||||
|
final String actionText;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<HomeLotteryBanner> createState() => _HomeLotteryBannerState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _HomeLotteryBannerState extends State<HomeLotteryBanner>
|
||||||
|
with TickerProviderStateMixin {
|
||||||
|
late AnimationController _pulseController;
|
||||||
|
late AnimationController _shimmerController;
|
||||||
|
late AnimationController _floatingController;
|
||||||
|
late Animation<double> _pulseAnimation;
|
||||||
|
late Animation<double> _shimmerAnimation;
|
||||||
|
late Animation<double> _floatingAnimation;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
|
||||||
|
if (widget.showAnimation) {
|
||||||
|
// Pulse animation for the whole banner
|
||||||
|
_pulseController = AnimationController(
|
||||||
|
duration: const Duration(seconds: 2),
|
||||||
|
vsync: this,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Shimmer effect for the gradient
|
||||||
|
_shimmerController = AnimationController(
|
||||||
|
duration: const Duration(seconds: 3),
|
||||||
|
vsync: this,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Floating animation for the icon
|
||||||
|
_floatingController = AnimationController(
|
||||||
|
duration: const Duration(seconds: 4),
|
||||||
|
vsync: this,
|
||||||
|
);
|
||||||
|
|
||||||
|
_pulseAnimation = Tween<double>(begin: 1.0, end: 1.02).animate(
|
||||||
|
CurvedAnimation(parent: _pulseController, curve: Curves.easeInOut),
|
||||||
|
);
|
||||||
|
|
||||||
|
_shimmerAnimation = Tween<double>(begin: -2.0, end: 2.0).animate(
|
||||||
|
CurvedAnimation(parent: _shimmerController, curve: Curves.easeInOut),
|
||||||
|
);
|
||||||
|
|
||||||
|
_floatingAnimation = Tween<double>(begin: -5.0, end: 5.0).animate(
|
||||||
|
CurvedAnimation(parent: _floatingController, curve: Curves.easeInOut),
|
||||||
|
);
|
||||||
|
|
||||||
|
_pulseController.repeat(reverse: true);
|
||||||
|
_shimmerController.repeat(reverse: true);
|
||||||
|
_floatingController.repeat(reverse: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
if (widget.showAnimation) {
|
||||||
|
_pulseController.dispose();
|
||||||
|
_shimmerController.dispose();
|
||||||
|
_floatingController.dispose();
|
||||||
|
}
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
Widget banner = Container(
|
||||||
|
margin: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: AppColor.primary.withOpacity(0.4),
|
||||||
|
blurRadius: 20,
|
||||||
|
offset: const Offset(0, 8),
|
||||||
|
spreadRadius: 0,
|
||||||
|
),
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.orange.withOpacity(0.2),
|
||||||
|
blurRadius: 40,
|
||||||
|
offset: const Offset(0, 16),
|
||||||
|
spreadRadius: 0,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
// Main gradient background
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(20),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: LinearGradient(
|
||||||
|
colors: [
|
||||||
|
AppColor.primary,
|
||||||
|
Colors.orange.shade600,
|
||||||
|
Colors.red.shade500,
|
||||||
|
],
|
||||||
|
begin: Alignment.topLeft,
|
||||||
|
end: Alignment.bottomRight,
|
||||||
|
stops: const [0.0, 0.6, 1.0],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
// Top section with icon and text
|
||||||
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
// Animated floating icon with multiple effects
|
||||||
|
widget.showAnimation
|
||||||
|
? AnimatedBuilder(
|
||||||
|
animation: _floatingAnimation,
|
||||||
|
builder: (context, child) {
|
||||||
|
return Transform.translate(
|
||||||
|
offset: Offset(0, _floatingAnimation.value),
|
||||||
|
child: _buildIcon(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
: _buildIcon(),
|
||||||
|
|
||||||
|
const SizedBox(width: 20),
|
||||||
|
|
||||||
|
// Enhanced text section - now expanded fully
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
widget.title,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.w900,
|
||||||
|
color: Colors.white,
|
||||||
|
letterSpacing: 0.5,
|
||||||
|
shadows: [
|
||||||
|
Shadow(
|
||||||
|
offset: Offset(0, 2),
|
||||||
|
blurRadius: 4,
|
||||||
|
color: Colors.black26,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
widget.subtitle,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 13,
|
||||||
|
color: Colors.white.withOpacity(0.95),
|
||||||
|
height: 1.2,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// Bottom action button - full width
|
||||||
|
_buildActionButton(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Shimmer overlay effect
|
||||||
|
if (widget.showAnimation)
|
||||||
|
AnimatedBuilder(
|
||||||
|
animation: _shimmerAnimation,
|
||||||
|
builder: (context, child) {
|
||||||
|
return Positioned.fill(
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: LinearGradient(
|
||||||
|
colors: [
|
||||||
|
Colors.transparent,
|
||||||
|
Colors.white.withOpacity(0.1),
|
||||||
|
Colors.transparent,
|
||||||
|
],
|
||||||
|
stops: const [0.0, 0.5, 1.0],
|
||||||
|
begin: Alignment(_shimmerAnimation.value, -1),
|
||||||
|
end: Alignment(_shimmerAnimation.value + 0.5, 1),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
|
||||||
|
// Decorative dots pattern
|
||||||
|
Positioned(
|
||||||
|
top: -20,
|
||||||
|
right: -20,
|
||||||
|
child: Container(
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
color: Colors.white.withOpacity(0.05),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
bottom: -10,
|
||||||
|
left: -30,
|
||||||
|
child: Container(
|
||||||
|
width: 60,
|
||||||
|
height: 60,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
color: Colors.orange.withOpacity(0.1),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Wrap with gesture detector and animations
|
||||||
|
if (widget.onTap != null) {
|
||||||
|
banner = GestureDetector(onTap: widget.onTap, child: banner);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (widget.showAnimation) {
|
||||||
|
return AnimatedBuilder(
|
||||||
|
animation: _pulseAnimation,
|
||||||
|
builder: (context, child) {
|
||||||
|
return Transform.scale(scale: _pulseAnimation.value, child: banner);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return banner;
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildIcon() {
|
||||||
|
return Container(
|
||||||
|
width: 64,
|
||||||
|
height: 64,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: RadialGradient(
|
||||||
|
colors: [
|
||||||
|
Colors.yellow.shade300,
|
||||||
|
Colors.orange.shade400,
|
||||||
|
Colors.red.shade500,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.orange.withOpacity(0.6),
|
||||||
|
blurRadius: 12,
|
||||||
|
spreadRadius: 2,
|
||||||
|
),
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.yellow.withOpacity(0.3),
|
||||||
|
blurRadius: 20,
|
||||||
|
spreadRadius: 4,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
const Center(
|
||||||
|
child: Icon(
|
||||||
|
Icons.casino,
|
||||||
|
color: Colors.white,
|
||||||
|
size: 32,
|
||||||
|
shadows: [
|
||||||
|
Shadow(
|
||||||
|
offset: Offset(0, 2),
|
||||||
|
blurRadius: 4,
|
||||||
|
color: Colors.black26,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// Sparkle effects
|
||||||
|
Positioned(
|
||||||
|
top: 8,
|
||||||
|
right: 8,
|
||||||
|
child: Container(
|
||||||
|
width: 8,
|
||||||
|
height: 8,
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
color: Colors.white,
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.white,
|
||||||
|
blurRadius: 4,
|
||||||
|
spreadRadius: 1,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
bottom: 10,
|
||||||
|
left: 10,
|
||||||
|
child: Container(
|
||||||
|
width: 4,
|
||||||
|
height: 4,
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
color: Colors.white70,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildActionButton() {
|
||||||
|
return Container(
|
||||||
|
width: double.infinity,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: LinearGradient(
|
||||||
|
colors: [Colors.white, Colors.yellow.shade100],
|
||||||
|
begin: Alignment.topCenter,
|
||||||
|
end: Alignment.bottomCenter,
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.circular(25),
|
||||||
|
border: Border.all(color: Colors.white.withOpacity(0.3), width: 1),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withOpacity(0.2),
|
||||||
|
blurRadius: 8,
|
||||||
|
offset: const Offset(0, 4),
|
||||||
|
),
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.white.withOpacity(0.5),
|
||||||
|
blurRadius: 4,
|
||||||
|
offset: const Offset(0, -1),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
widget.actionText,
|
||||||
|
style: TextStyle(
|
||||||
|
color: AppColor.primary,
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.w800,
|
||||||
|
letterSpacing: 0.5,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(3),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
color: AppColor.primary.withOpacity(0.1),
|
||||||
|
),
|
||||||
|
child: Icon(
|
||||||
|
Icons.arrow_forward_rounded,
|
||||||
|
color: AppColor.primary,
|
||||||
|
size: 16,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
227
lib/presentation/pages/main/pages/home/widgets/point_card.dart
Normal file
227
lib/presentation/pages/main/pages/home/widgets/point_card.dart
Normal file
@ -0,0 +1,227 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import '../../../../../../common/theme/theme.dart';
|
||||||
|
|
||||||
|
class HomePointCard extends StatelessWidget {
|
||||||
|
const HomePointCard({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppColor.white,
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: AppColor.textLight.withOpacity(0.15),
|
||||||
|
spreadRadius: 0,
|
||||||
|
blurRadius: 20,
|
||||||
|
offset: const Offset(0, 8),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
_buildCoinPattern(),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 16,
|
||||||
|
vertical: 10,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppColor.primary,
|
||||||
|
borderRadius: BorderRadius.circular(25),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.stars,
|
||||||
|
color: AppColor.white,
|
||||||
|
size: 18,
|
||||||
|
),
|
||||||
|
SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
'148 Poin',
|
||||||
|
style: AppStyle.md.copyWith(
|
||||||
|
color: AppColor.white,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
'Kamu punya 148 poin',
|
||||||
|
style: AppStyle.sm.copyWith(
|
||||||
|
color: AppColor.textSecondary,
|
||||||
|
fontSize: 11,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
SizedBox(
|
||||||
|
width: 120,
|
||||||
|
height: 40,
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
_buildCoin(
|
||||||
|
right: 0,
|
||||||
|
top: 0,
|
||||||
|
size: 24,
|
||||||
|
color: Colors.amber,
|
||||||
|
),
|
||||||
|
_buildCoin(
|
||||||
|
right: 20,
|
||||||
|
top: 8,
|
||||||
|
size: 20,
|
||||||
|
color: Colors.orange,
|
||||||
|
),
|
||||||
|
_buildCoin(
|
||||||
|
right: 40,
|
||||||
|
top: 4,
|
||||||
|
size: 18,
|
||||||
|
color: Colors.amber,
|
||||||
|
),
|
||||||
|
_buildCoin(
|
||||||
|
right: 60,
|
||||||
|
top: 12,
|
||||||
|
size: 16,
|
||||||
|
color: Colors.orange,
|
||||||
|
),
|
||||||
|
_buildCoin(
|
||||||
|
right: 80,
|
||||||
|
top: 8,
|
||||||
|
size: 14,
|
||||||
|
color: Colors.amber,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Tukarkan poinmu dengan hadiah menarik',
|
||||||
|
style: AppStyle.sm.copyWith(
|
||||||
|
color: AppColor.textPrimary,
|
||||||
|
fontSize: 13,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
Icon(
|
||||||
|
Icons.arrow_forward_ios,
|
||||||
|
color: AppColor.textSecondary,
|
||||||
|
size: 16,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildCoinPattern() {
|
||||||
|
return Positioned.fill(
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
Positioned(
|
||||||
|
right: -20,
|
||||||
|
top: -10,
|
||||||
|
child: Container(
|
||||||
|
width: 60,
|
||||||
|
height: 60,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.amber.withOpacity(0.1),
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
right: 40,
|
||||||
|
top: 30,
|
||||||
|
child: Container(
|
||||||
|
width: 20,
|
||||||
|
height: 20,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.orange.withOpacity(0.15),
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
left: -15,
|
||||||
|
bottom: -20,
|
||||||
|
child: Container(
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.amber.withOpacity(0.08),
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
left: 60,
|
||||||
|
bottom: 10,
|
||||||
|
child: Container(
|
||||||
|
width: 15,
|
||||||
|
height: 15,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.orange.withOpacity(0.12),
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildCoin({
|
||||||
|
required double right,
|
||||||
|
required double top,
|
||||||
|
required double size,
|
||||||
|
required Color color,
|
||||||
|
}) {
|
||||||
|
return Positioned(
|
||||||
|
right: right,
|
||||||
|
top: top,
|
||||||
|
child: Container(
|
||||||
|
width: size,
|
||||||
|
height: size,
|
||||||
|
decoration: BoxDecoration(color: color, shape: BoxShape.circle),
|
||||||
|
child: Center(
|
||||||
|
child: Text(
|
||||||
|
'\$',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: size * 0.5,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,177 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import '../../../../../../common/theme/theme.dart';
|
||||||
|
import '../../../../../components/image/image.dart';
|
||||||
|
|
||||||
|
class HomePopularMerchantCard extends StatelessWidget {
|
||||||
|
final String merchantName;
|
||||||
|
final String merchantImage;
|
||||||
|
final String category;
|
||||||
|
final double rating;
|
||||||
|
final String distance;
|
||||||
|
final bool isOpen;
|
||||||
|
final VoidCallback? onTap;
|
||||||
|
|
||||||
|
const HomePopularMerchantCard({
|
||||||
|
super.key,
|
||||||
|
required this.merchantName,
|
||||||
|
required this.merchantImage,
|
||||||
|
required this.category,
|
||||||
|
required this.rating,
|
||||||
|
required this.distance,
|
||||||
|
this.isOpen = true,
|
||||||
|
this.onTap,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: onTap,
|
||||||
|
child: Container(
|
||||||
|
margin: const EdgeInsets.only(bottom: 12),
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppColor.surface,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: AppColor.black.withOpacity(0.06),
|
||||||
|
blurRadius: 8,
|
||||||
|
offset: const Offset(0, 2),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
// Image Container
|
||||||
|
Container(
|
||||||
|
width: 60,
|
||||||
|
height: 60,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
color: AppColor.border,
|
||||||
|
),
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
child: Image.network(
|
||||||
|
merchantImage,
|
||||||
|
width: 60,
|
||||||
|
height: 60,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
errorBuilder: (context, error, stackTrace) {
|
||||||
|
return ImagePlaceholder(width: 60, height: 60);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
|
||||||
|
// Title and Category
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
merchantName,
|
||||||
|
style: AppStyle.md.copyWith(
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
height: 1.2,
|
||||||
|
),
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
|
||||||
|
Text(
|
||||||
|
category,
|
||||||
|
style: AppStyle.sm.copyWith(color: AppColor.textSecondary),
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
|
||||||
|
// Distance
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.location_on,
|
||||||
|
size: 12,
|
||||||
|
color: AppColor.textSecondary,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 2),
|
||||||
|
Text(
|
||||||
|
distance,
|
||||||
|
style: AppStyle.xs.copyWith(
|
||||||
|
color: AppColor.textSecondary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
|
||||||
|
// Rating and Status
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
// Status Badge
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 6,
|
||||||
|
vertical: 2,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: isOpen ? AppColor.success : AppColor.error,
|
||||||
|
borderRadius: BorderRadius.circular(6),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
isOpen ? 'OPEN' : 'CLOSED',
|
||||||
|
style: AppStyle.xs.copyWith(
|
||||||
|
color: AppColor.textWhite,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
fontSize: 10,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
|
||||||
|
// Rating
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 6,
|
||||||
|
vertical: 3,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppColor.primary.withOpacity(0.1),
|
||||||
|
borderRadius: BorderRadius.circular(6),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Icon(Icons.star, size: 12, color: AppColor.warning),
|
||||||
|
const SizedBox(width: 2),
|
||||||
|
Text(
|
||||||
|
rating.toString(),
|
||||||
|
style: AppStyle.xs.copyWith(
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: AppColor.primary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,81 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import '../../../../../../common/theme/theme.dart';
|
||||||
|
import 'popular_merchant_card.dart';
|
||||||
|
|
||||||
|
class HomePopularMerchantSection extends StatelessWidget {
|
||||||
|
const HomePopularMerchantSection({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 24),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Popular Merchants',
|
||||||
|
style: AppStyle.xl.copyWith(fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
Spacer(),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Lihat Semua',
|
||||||
|
style: AppStyle.sm.copyWith(
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color: AppColor.primary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(width: 4),
|
||||||
|
Icon(
|
||||||
|
Icons.arrow_forward_ios,
|
||||||
|
size: 12,
|
||||||
|
color: AppColor.primary,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
SizedBox(height: 16),
|
||||||
|
HomePopularMerchantCard(
|
||||||
|
merchantName: 'Warung Bu Sari',
|
||||||
|
merchantImage: 'https://via.placeholder.com/280x160',
|
||||||
|
category: 'Indonesian Food',
|
||||||
|
rating: 4.8,
|
||||||
|
distance: '0.5 km',
|
||||||
|
isOpen: true,
|
||||||
|
onTap: () {
|
||||||
|
print('Warung Bu Sari tapped');
|
||||||
|
},
|
||||||
|
),
|
||||||
|
|
||||||
|
HomePopularMerchantCard(
|
||||||
|
merchantName: 'Pizza Corner',
|
||||||
|
merchantImage: 'https://via.placeholder.com/280x160',
|
||||||
|
category: 'Italian Food',
|
||||||
|
rating: 4.6,
|
||||||
|
distance: '1.2 km',
|
||||||
|
isOpen: false,
|
||||||
|
onTap: () {
|
||||||
|
print('Pizza Corner tapped');
|
||||||
|
},
|
||||||
|
),
|
||||||
|
|
||||||
|
HomePopularMerchantCard(
|
||||||
|
merchantName: 'Kopi Kenangan',
|
||||||
|
merchantImage: 'https://via.placeholder.com/280x160',
|
||||||
|
category: 'Coffee & Drinks',
|
||||||
|
rating: 4.9,
|
||||||
|
distance: '0.8 km',
|
||||||
|
isOpen: true,
|
||||||
|
onTap: () {
|
||||||
|
print('Kopi Kenangan tapped');
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user