feat: home page
This commit is contained in:
parent
c3eb62077f
commit
627de219cb
@ -1,4 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'dart:math' as math;
|
||||
|
||||
import '../../../common/theme/theme.dart';
|
||||
import '../assets/assets.gen.dart';
|
||||
|
||||
@ -1,71 +1,161 @@
|
||||
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 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: double.infinity,
|
||||
decoration: const BoxDecoration(
|
||||
color: Color(0x4DD9D9D9), // Light gray with opacity
|
||||
borderRadius: BorderRadius.only(
|
||||
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),
|
||||
),
|
||||
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,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Hand holding coffee illustration
|
||||
Container(
|
||||
width: 120,
|
||||
height: 160,
|
||||
width: iconSize * 1.5,
|
||||
height: iconSize * 1.5,
|
||||
decoration: BoxDecoration(
|
||||
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(
|
||||
children: [
|
||||
// Hand
|
||||
Positioned(
|
||||
bottom: 20,
|
||||
left: 30,
|
||||
bottom: illustrationHeight * 0.125, // 20/160 ratio
|
||||
left: illustrationSize * 0.25, // 30/120 ratio
|
||||
child: Container(
|
||||
width: 60,
|
||||
height: 80,
|
||||
width: handWidth,
|
||||
height: handHeight,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFFFDBB3),
|
||||
borderRadius: BorderRadius.circular(30),
|
||||
borderRadius: BorderRadius.circular(handWidth / 2),
|
||||
),
|
||||
),
|
||||
),
|
||||
// Coffee cup
|
||||
Positioned(
|
||||
top: 30,
|
||||
left: 25,
|
||||
top: illustrationHeight * 0.1875, // 30/160 ratio
|
||||
left: illustrationSize * 0.208, // 25/120 ratio
|
||||
child: Container(
|
||||
width: 70,
|
||||
height: 90,
|
||||
width: cupWidth,
|
||||
height: cupHeight,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF4E4BC),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
borderRadius: BorderRadius.circular(
|
||||
math.max(8.0, 10 * scaleFactor),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
// Fore logo
|
||||
// Logo
|
||||
Assets.images.logo.image(
|
||||
width: 40,
|
||||
height: 40,
|
||||
width: logoSize,
|
||||
height: logoSize,
|
||||
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(
|
||||
'Enaklo',
|
||||
style: TextStyle(
|
||||
color: AppColor.primary,
|
||||
fontSize: 12,
|
||||
fontSize: fontSize,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
@ -75,9 +165,6 @@ class ImagePlaceholder extends StatelessWidget {
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<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
|
||||
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