diff --git a/lib/presentation/components/image/image.dart b/lib/presentation/components/image/image.dart index c355993..e4f8ffa 100644 --- a/lib/presentation/components/image/image.dart +++ b/lib/presentation/components/image/image.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'dart:math' as math; import '../../../common/theme/theme.dart'; import '../assets/assets.gen.dart'; diff --git a/lib/presentation/components/image/image_placeholder.dart b/lib/presentation/components/image/image_placeholder.dart index 11bab48..a4183e3 100644 --- a/lib/presentation/components/image/image_placeholder.dart +++ b/lib/presentation/components/image/image_placeholder.dart @@ -1,79 +1,166 @@ part of 'image.dart'; 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 Widget build(BuildContext context) { - return Container( - width: double.infinity, - decoration: const BoxDecoration( - color: Color(0x4DD9D9D9), // Light gray with opacity - borderRadius: BorderRadius.only( - bottomLeft: Radius.circular(20), - bottomRight: Radius.circular(20), - ), - ), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - // Hand holding coffee illustration - Container( - width: 120, - height: 160, - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(60), + 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( + width: containerWidth == double.infinity + ? double.infinity + : containerWidth, + height: containerHeight == double.infinity ? null : containerHeight, + decoration: BoxDecoration( + 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 - Positioned( - bottom: 20, - left: 30, - child: Container( - width: 60, - height: 80, - decoration: BoxDecoration( - color: const Color(0xFFFFDBB3), - borderRadius: BorderRadius.circular(30), - ), - ), + ), + ), + 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( + 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( - top: 30, - left: 25, - child: Container( - width: 70, - height: 90, - decoration: BoxDecoration( - color: const Color(0xFFF4E4BC), - 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, - ), - ), - ], - ), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // Logo + Assets.images.logo.image( + width: logoSize, + height: logoSize, + fit: BoxFit.contain, ), - ), - ], + 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, + ), + ), + ], + ), ), ), ], diff --git a/lib/presentation/pages/main/pages/home/home_page.dart b/lib/presentation/pages/main/pages/home/home_page.dart index 01c68aa..d5c074b 100644 --- a/lib/presentation/pages/main/pages/home/home_page.dart +++ b/lib/presentation/pages/main/pages/home/home_page.dart @@ -1,12 +1,171 @@ import 'package:auto_route/auto_route.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() -class HomePage extends StatelessWidget { +class HomePage extends StatefulWidget { const HomePage({super.key}); + @override + State createState() => _HomePageState(); +} + +class _HomePageState extends State { + int _currentCarouselIndex = 0; + final CarouselSliderController _carouselController = + CarouselSliderController(); + + final List _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 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, + ), + ), + ); + }), + ); } } diff --git a/lib/presentation/pages/main/pages/home/widgets/feature_card.dart b/lib/presentation/pages/main/pages/home/widgets/feature_card.dart new file mode 100644 index 0000000..83b715f --- /dev/null +++ b/lib/presentation/pages/main/pages/home/widgets/feature_card.dart @@ -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 createState() => _HomeFeatureCardState(); +} + +class _HomeFeatureCardState extends State + with SingleTickerProviderStateMixin { + late AnimationController _controller; + late Animation _scaleAnimation; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + duration: const Duration(milliseconds: 100), + vsync: this, + ); + _scaleAnimation = Tween( + 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, + ), + ], + ), + ), + ); + }, + ), + ); + } +} diff --git a/lib/presentation/pages/main/pages/home/widgets/feature_section.dart b/lib/presentation/pages/main/pages/home/widgets/feature_section.dart new file mode 100644 index 0000000..d77092c --- /dev/null +++ b/lib/presentation/pages/main/pages/home/widgets/feature_section.dart @@ -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'), + ), + ], + ), + ); + } +} diff --git a/lib/presentation/pages/main/pages/home/widgets/lottery_card.dart b/lib/presentation/pages/main/pages/home/widgets/lottery_card.dart new file mode 100644 index 0000000..f77d1c6 --- /dev/null +++ b/lib/presentation/pages/main/pages/home/widgets/lottery_card.dart @@ -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 createState() => _HomeLotteryBannerState(); +} + +class _HomeLotteryBannerState extends State + with TickerProviderStateMixin { + late AnimationController _pulseController; + late AnimationController _shimmerController; + late AnimationController _floatingController; + late Animation _pulseAnimation; + late Animation _shimmerAnimation; + late Animation _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(begin: 1.0, end: 1.02).animate( + CurvedAnimation(parent: _pulseController, curve: Curves.easeInOut), + ); + + _shimmerAnimation = Tween(begin: -2.0, end: 2.0).animate( + CurvedAnimation(parent: _shimmerController, curve: Curves.easeInOut), + ); + + _floatingAnimation = Tween(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, + ), + ), + ], + ), + ); + } +} diff --git a/lib/presentation/pages/main/pages/home/widgets/point_card.dart b/lib/presentation/pages/main/pages/home/widgets/point_card.dart new file mode 100644 index 0000000..b7cd9d1 --- /dev/null +++ b/lib/presentation/pages/main/pages/home/widgets/point_card.dart @@ -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, + ), + ), + ), + ), + ); + } +} diff --git a/lib/presentation/pages/main/pages/home/widgets/popular_merchant_card.dart b/lib/presentation/pages/main/pages/home/widgets/popular_merchant_card.dart new file mode 100644 index 0000000..1ad3405 --- /dev/null +++ b/lib/presentation/pages/main/pages/home/widgets/popular_merchant_card.dart @@ -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, + ), + ), + ], + ), + ), + ], + ), + ], + ), + ), + ); + } +} diff --git a/lib/presentation/pages/main/pages/home/widgets/popular_merchant_section.dart b/lib/presentation/pages/main/pages/home/widgets/popular_merchant_section.dart new file mode 100644 index 0000000..d954c90 --- /dev/null +++ b/lib/presentation/pages/main/pages/home/widgets/popular_merchant_section.dart @@ -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'); + }, + ), + ], + ), + ); + } +}