feat: home page

This commit is contained in:
efrilm 2025-08-28 00:40:18 +07:00
parent c3eb62077f
commit 627de219cb
9 changed files with 1333 additions and 69 deletions

View File

@ -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';

View File

@ -1,79 +1,166 @@
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 Container( return LayoutBuilder(
width: double.infinity, builder: (context, constraints) {
decoration: const BoxDecoration( // Determine the size based on available space or provided dimensions
color: Color(0x4DD9D9D9), // Light gray with opacity final containerWidth = width ?? constraints.maxWidth;
borderRadius: BorderRadius.only( final containerHeight = height ?? constraints.maxHeight;
bottomLeft: Radius.circular(20),
bottomRight: Radius.circular(20), // Calculate the minimum dimension to determine if we should show simple or detailed version
), final minDimension = math.min(
), containerWidth == double.infinity ? containerHeight : containerWidth,
child: Column( containerHeight == double.infinity ? containerWidth : containerHeight,
mainAxisAlignment: MainAxisAlignment.center, );
children: [
// Hand holding coffee illustration return Container(
Container( width: containerWidth == double.infinity
width: 120, ? double.infinity
height: 160, : containerWidth,
decoration: BoxDecoration( height: containerHeight == double.infinity ? null : containerHeight,
color: Colors.white, decoration: BoxDecoration(
borderRadius: BorderRadius.circular(60), color: backgroundColor ?? const Color(0x4DD9D9D9),
borderRadius: showBorderRadius
? const BorderRadius.only(
bottomLeft: Radius.circular(20),
bottomRight: Radius.circular(20),
)
: null,
),
child: Center(
child: minDimension < 100
? _buildSimpleVersion(minDimension)
: _buildDetailedVersion(minDimension),
),
);
},
);
}
// 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,
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: iconSize * 1.5,
height: iconSize * 1.5,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(iconSize * 0.75),
),
child: Center(
child: Assets.images.logo.image(
width: iconSize,
height: iconSize,
fit: BoxFit.contain,
), ),
child: Stack( ),
children: [ ),
// Hand if (size > 50) ...[
Positioned( SizedBox(height: size * 0.05),
bottom: 20, Text(
left: 30, 'Enaklo',
child: Container( style: TextStyle(
width: 60, color: AppColor.primary,
height: 80, fontSize: fontSize,
decoration: BoxDecoration( fontWeight: FontWeight.bold,
color: const Color(0xFFFFDBB3), ),
borderRadius: BorderRadius.circular(30), ),
), ],
), ],
);
}
// 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(
children: [
// Hand
Positioned(
bottom: illustrationHeight * 0.125, // 20/160 ratio
left: illustrationSize * 0.25, // 30/120 ratio
child: Container(
width: handWidth,
height: handHeight,
decoration: BoxDecoration(
color: const Color(0xFFFFDBB3),
borderRadius: BorderRadius.circular(handWidth / 2),
),
),
),
// Coffee cup
Positioned(
top: illustrationHeight * 0.1875, // 30/160 ratio
left: illustrationSize * 0.208, // 25/120 ratio
child: Container(
width: cupWidth,
height: cupHeight,
decoration: BoxDecoration(
color: const Color(0xFFF4E4BC),
borderRadius: BorderRadius.circular(
math.max(8.0, 10 * scaleFactor),
), ),
// Coffee cup ),
Positioned( child: Column(
top: 30, mainAxisAlignment: MainAxisAlignment.center,
left: 25, children: [
child: Container( // Logo
width: 70, Assets.images.logo.image(
height: 90, width: logoSize,
decoration: BoxDecoration( height: logoSize,
color: const Color(0xFFF4E4BC), fit: BoxFit.contain,
borderRadius: BorderRadius.circular(10),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Fore logo
Assets.images.logo.image(
width: 40,
height: 40,
fit: BoxFit.contain,
),
const SizedBox(height: 8),
Text(
'Enaklo',
style: TextStyle(
color: AppColor.primary,
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
],
),
), ),
), SizedBox(height: math.max(4.0, 8 * scaleFactor)),
], if (cupHeight > 50) // Only show text if cup is big enough
Text(
'Enaklo',
style: TextStyle(
color: AppColor.primary,
fontSize: fontSize,
fontWeight: FontWeight.bold,
),
),
],
),
), ),
), ),
], ],

View File

@ -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,
),
),
);
}),
);
} }
} }

View 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,
),
],
),
),
);
},
),
);
}
}

View File

@ -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'),
),
],
),
);
}
}

View 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,
),
),
],
),
);
}
}

View 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,
),
),
),
),
);
}
}

View File

@ -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,
),
),
],
),
),
],
),
],
),
),
);
}
}

View File

@ -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');
},
),
],
),
);
}
}