Compare commits

...

7 Commits

Author SHA1 Message Date
efrilm
627de219cb feat: home page 2025-08-28 00:40:18 +07:00
efrilm
c3eb62077f feat: profile page 2025-08-27 20:14:06 +07:00
efrilm
d3120c1bf2 feat: profile page 2025-08-27 20:12:38 +07:00
efrilm
09d8f6af69 feat: order page 2025-08-27 20:02:49 +07:00
efrilm
8412220a06 feat: voucher card 2025-08-27 19:37:22 +07:00
efrilm
8312429be3 feat: main page 2025-08-27 18:51:14 +07:00
efrilm
2d29a2f38a feat: pin page 2025-08-27 18:40:03 +07:00
31 changed files with 3746 additions and 100 deletions

View File

@ -26,6 +26,7 @@ class ThemeApp {
),
centerTitle: true,
iconTheme: IconThemeData(color: AppColor.primary),
scrolledUnderElevation: 0.0,
),
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
@ -53,5 +54,12 @@ class ThemeApp {
),
contentPadding: const EdgeInsets.symmetric(vertical: 12),
),
bottomNavigationBarTheme: BottomNavigationBarThemeData(
type: BottomNavigationBarType.fixed,
selectedItemColor: AppColor.primary,
unselectedItemColor: AppColor.textSecondary,
backgroundColor: AppColor.white,
elevation: 4,
),
);
}

View File

@ -0,0 +1,61 @@
import 'dart:ui';
import 'package:flutter/material.dart';
class VoucherClipper extends CustomClipper<Path> {
@override
Path getClip(Size size) {
final path = Path();
double notchRadius = 10.0;
double notchPosition = size.height * 0.6; // Position of notch
// Start from top-left
path.moveTo(0, 12);
// Top edge with rounded corners
path.quadraticBezierTo(0, 0, 12, 0);
path.lineTo(size.width - 12, 0);
path.quadraticBezierTo(size.width, 0, size.width, 12);
// Right edge until notch
path.lineTo(size.width, notchPosition - notchRadius);
// Right notch (semicircle going inward)
path.arcToPoint(
Offset(size.width, notchPosition + notchRadius),
radius: Radius.circular(notchRadius),
clockwise: false,
);
// Continue right edge
path.lineTo(size.width, size.height - 12);
// Bottom edge
path.quadraticBezierTo(
size.width,
size.height,
size.width - 12,
size.height,
);
path.lineTo(12, size.height);
path.quadraticBezierTo(0, size.height, 0, size.height - 12);
// Left edge until notch
path.lineTo(0, notchPosition + notchRadius);
// Left notch (semicircle going inward)
path.arcToPoint(
Offset(0, notchPosition - notchRadius),
radius: Radius.circular(notchRadius),
clockwise: false,
);
// Close path
path.close();
return path;
}
@override
bool shouldReclip(covariant CustomClipper<Path> oldClipper) => false;
}

View File

@ -0,0 +1,27 @@
import 'package:flutter/material.dart';
class DashedLinePainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
Paint paint = Paint()
..color = Colors.grey[300]!
..strokeWidth = 1
..style = PaintingStyle.stroke;
double dashWidth = 5;
double dashSpace = 3;
double startX = 0;
while (startX < size.width) {
canvas.drawLine(
Offset(startX, size.height / 2),
Offset(startX + dashWidth, size.height / 2),
paint,
);
startX += dashWidth + dashSpace;
}
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}

View File

@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'dart:math' as math;
import '../../../common/theme/theme.dart';
import '../assets/assets.gen.dart';

View File

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

View File

@ -5,6 +5,7 @@ import 'package:flutter/services.dart';
import '../../../../common/theme/theme.dart';
import '../../../components/button/button.dart';
import '../../../router/app_router.gr.dart';
@RoutePage()
class OtpPage extends StatefulWidget {
@ -82,8 +83,7 @@ class _OtpPageState extends State<OtpPage> {
void _verifyCode() {
String code = _controllers.map((controller) => controller.text).join();
if (code.length == 6) {
// Add your verification logic here
print('Verifying code: $code');
context.router.push(PinRoute(isCreatePin: true));
}
}

View File

@ -0,0 +1,331 @@
import 'dart:async';
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../../../../common/theme/theme.dart';
import '../../../components/button/button.dart';
import '../../../router/app_router.gr.dart';
@RoutePage()
class PinPage extends StatefulWidget {
final bool isCreatePin; // true for creating PIN, false for entering PIN
final String? title; // Optional custom title
const PinPage({super.key, this.isCreatePin = true, this.title});
@override
State<PinPage> createState() => _PinPageState();
}
class _PinPageState extends State<PinPage> {
final List<TextEditingController> _controllers = List.generate(
6,
(index) => TextEditingController(),
);
final List<FocusNode> _focusNodes = List.generate(6, (index) => FocusNode());
String _firstPin = '';
bool _isConfirmingPin = false;
bool _isPinMismatch = false;
@override
void initState() {
super.initState();
}
void _onPinChanged(String value, int index) {
if (value.isNotEmpty) {
// Move to next field
if (index < 5) {
_focusNodes[index + 1].requestFocus();
} else {
// Last field, unfocus and process PIN
_focusNodes[index].unfocus();
_processPinInput();
}
} else {
// Handle backspace - move to previous field
if (index > 0) {
_focusNodes[index - 1].requestFocus();
}
}
}
void _processPinInput() {
String currentPin = _controllers
.map((controller) => controller.text)
.join();
if (currentPin.length == 6) {
if (widget.isCreatePin) {
if (!_isConfirmingPin) {
// First PIN entry - store and ask for confirmation
_firstPin = currentPin;
setState(() {
_isConfirmingPin = true;
_isPinMismatch = false;
});
_clearPinFields();
} else {
// Confirming PIN
if (currentPin == _firstPin) {
// PINs match - create PIN
_createPin(currentPin);
} else {
// PINs don't match
setState(() {
_isPinMismatch = true;
});
_clearPinFields();
// Auto-hide error after 2 seconds
Timer(const Duration(seconds: 2), () {
if (mounted) {
setState(() {
_isPinMismatch = false;
});
}
});
}
}
} else {
// Entering existing PIN
_verifyPin(currentPin);
}
}
context.router.push(MainRoute());
}
void _clearPinFields() {
for (var controller in _controllers) {
controller.clear();
}
_focusNodes[0].requestFocus();
}
void _createPin(String pin) {
// Add your PIN creation logic here
print('Creating PIN: $pin');
// Navigate to next screen or show success message
}
void _verifyPin(String pin) {
// Add your PIN verification logic here
print('Verifying PIN: $pin');
// Navigate to next screen or show error
}
void _resetPinCreation() {
setState(() {
_isConfirmingPin = false;
_firstPin = '';
_isPinMismatch = false;
});
_clearPinFields();
}
String get _getTitle {
if (widget.title != null) return widget.title!;
if (widget.isCreatePin) {
return _isConfirmingPin ? 'Konfirmasi PIN' : 'Buat PIN Baru';
} else {
return 'Masukan PIN';
}
}
String get _getDescription {
if (widget.isCreatePin) {
if (_isConfirmingPin) {
return 'Masukan kembali PIN untuk konfirmasi';
} else {
return 'Buat PIN 6 digit untuk keamanan akun Anda';
}
} else {
return 'Masukan PIN 6 digit Anda untuk melanjutkan';
}
}
@override
void dispose() {
for (var controller in _controllers) {
controller.dispose();
}
for (var focusNode in _focusNodes) {
focusNode.dispose();
}
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.isCreatePin ? 'Buat PIN' : 'Masukan PIN'),
leading: widget.isCreatePin && _isConfirmingPin
? IconButton(
icon: Icon(Icons.arrow_back),
onPressed: _resetPinCreation,
)
: null,
),
body: Padding(
padding: const EdgeInsets.symmetric(horizontal: 20.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 20),
// Title
Text(
_getTitle,
style: AppStyle.xl.copyWith(
fontWeight: FontWeight.w600,
color: AppColor.textPrimary,
),
),
const SizedBox(height: 12),
// Description
Text(
_getDescription,
style: AppStyle.sm.copyWith(
color: AppColor.textSecondary,
height: 1.4,
),
),
// Error message for PIN mismatch
if (_isPinMismatch) ...[
const SizedBox(height: 8),
Text(
'PIN tidak sama. Silakan coba lagi.',
style: AppStyle.sm.copyWith(
color: AppColor.error,
fontWeight: FontWeight.w500,
height: 1.4,
),
),
],
const SizedBox(height: 40),
// PIN input fields
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: List.generate(6, (index) {
return Expanded(
child: Padding(
padding: EdgeInsets.only(right: index == 5 ? 0 : 8.0),
child: TextFormField(
controller: _controllers[index],
focusNode: _focusNodes[index],
keyboardType: TextInputType.number,
maxLength: 1,
obscureText: true, // Hide PIN input
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
decoration: InputDecoration(
counterText: '',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(
color: _isPinMismatch
? AppColor.error
: AppColor.border,
),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(
color: _isPinMismatch
? AppColor.error
: AppColor.primary,
width: 2,
),
),
errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(color: AppColor.error),
),
),
textAlign: TextAlign.center,
style: AppStyle.lg.copyWith(
color: AppColor.primary,
fontWeight: FontWeight.w600,
),
cursorColor: AppColor.primary,
onChanged: (value) {
setState(() {
_isPinMismatch = false;
});
_onPinChanged(value, index);
},
),
),
);
}),
),
const SizedBox(height: 40),
// Progress indicator for PIN creation
if (widget.isCreatePin) ...[
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
width: 8,
height: 8,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: AppColor.primary,
),
),
const SizedBox(width: 8),
Container(
width: 8,
height: 8,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: _isConfirmingPin
? AppColor.primary
: AppColor.border,
),
),
],
),
const SizedBox(height: 8),
Center(
child: Text(
_isConfirmingPin ? 'Langkah 2 dari 2' : 'Langkah 1 dari 2',
style: AppStyle.xs.copyWith(color: AppColor.textSecondary),
),
),
],
const Spacer(),
// Continue Button
AppElevatedButton(
title: widget.isCreatePin
? (_isConfirmingPin ? 'Konfirmasi' : 'Lanjutkan')
: 'Masuk',
onPressed: () {
String pin = _controllers
.map((controller) => controller.text)
.join();
if (pin.length == 6) {
_processPinInput();
}
},
),
const SizedBox(height: 24),
],
),
),
);
}
}

View File

@ -0,0 +1,24 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import '../../router/app_router.gr.dart';
import 'widgets/bottom_navbar.dart';
@RoutePage()
class MainPage extends StatelessWidget {
const MainPage({super.key});
@override
Widget build(BuildContext context) {
return AutoTabsRouter.pageView(
routes: [HomeRoute(), VoucherRoute(), OrderRoute(), ProfileRoute()],
physics: const NeverScrollableScrollPhysics(),
builder: (context, child, pageController) => Scaffold(
body: child,
bottomNavigationBar: MainBottomNavbar(
tabsRouter: AutoTabsRouter.of(context),
),
),
);
}
}

View File

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

View File

@ -0,0 +1,353 @@
import 'package:flutter/material.dart';
import '../../../../../common/theme/theme.dart';
import 'widgets/order_card.dart';
// Model untuk Order
class Order {
final String id;
final String customerName;
final DateTime orderDate;
final List<OrderItem> items;
final double totalAmount;
final OrderStatus status;
final String? notes;
final String? phoneNumber;
final String? address;
Order({
required this.id,
required this.customerName,
required this.orderDate,
required this.items,
required this.totalAmount,
required this.status,
this.notes,
this.phoneNumber,
this.address,
});
}
class OrderItem {
final String name;
final int quantity;
final double price;
final String? imageUrl;
final String? notes;
OrderItem({
required this.name,
required this.quantity,
required this.price,
this.imageUrl,
this.notes,
});
}
enum OrderStatus { pending, processing, completed, cancelled }
class OrderPage extends StatefulWidget {
const OrderPage({Key? key}) : super(key: key);
@override
State<OrderPage> createState() => _OrderPageState();
}
class _OrderPageState extends State<OrderPage> with TickerProviderStateMixin {
late TabController _tabController;
bool _isLoading = true;
List<Order> _orders = [];
// Filter states
@override
void initState() {
super.initState();
_tabController = TabController(length: 5, vsync: this);
_loadOrders();
}
@override
void dispose() {
_tabController.dispose();
super.dispose();
}
void _loadOrders() {
// Simulate loading
Future.delayed(const Duration(seconds: 2), () {
setState(() {
_isLoading = false;
// Uncomment untuk testing dengan data
_orders = _generateSampleOrders();
});
});
}
List<Order> _generateSampleOrders() {
return [
Order(
id: "ORD-001",
customerName: "John Doe",
orderDate: DateTime.now().subtract(const Duration(hours: 2)),
address: "Jl. Malioboro No. 123, Yogyakarta",
items: [
OrderItem(
name: "Nasi Gudeg",
quantity: 2,
price: 25000,
notes: "Pedas sedang",
),
OrderItem(name: "Es Teh Manis", quantity: 2, price: 8000),
OrderItem(name: "Kerupuk", quantity: 1, price: 5000),
],
totalAmount: 71000,
status: OrderStatus.pending,
notes: "Tolong diantar sebelum jam 2 siang",
),
Order(
id: "ORD-002",
customerName: "Jane Smith",
orderDate: DateTime.now().subtract(const Duration(hours: 1)),
address: "Jl. Sultan Agung No. 45, Yogyakarta",
items: [
OrderItem(
name: "Ayam Bakar",
quantity: 1,
price: 35000,
notes: "Tidak pedas",
),
OrderItem(name: "Nasi Putih", quantity: 1, price: 5000),
OrderItem(name: "Lalapan", quantity: 1, price: 8000),
],
totalAmount: 48000,
status: OrderStatus.processing,
),
Order(
id: "ORD-003",
customerName: "Bob Wilson",
orderDate: DateTime.now().subtract(const Duration(minutes: 30)),
phoneNumber: "+62 811-2345-6789",
items: [
OrderItem(name: "Gado-gado", quantity: 2, price: 20000),
OrderItem(name: "Lontong", quantity: 2, price: 3000),
OrderItem(name: "Es Jeruk", quantity: 2, price: 10000),
],
totalAmount: 66000,
status: OrderStatus.completed,
),
];
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppColor.background,
appBar: _buildAppBar(),
body: Column(
children: [
_buildTabBar(),
Expanded(
child: _isLoading ? _buildLoadingState() : _buildOrderContent(),
),
],
),
);
}
PreferredSizeWidget _buildAppBar() {
return AppBar(
elevation: 0,
backgroundColor: AppColor.white,
title: Text('Pesanan'),
actions: [
IconButton(
onPressed: _showFilterDialog,
icon: const Icon(Icons.filter_list, color: AppColor.textSecondary),
),
IconButton(
onPressed: _refreshOrders,
icon: const Icon(Icons.refresh, color: AppColor.textSecondary),
),
],
);
}
Widget _buildTabBar() {
return Container(
color: AppColor.white,
child: TabBar(
controller: _tabController,
isScrollable: true,
labelColor: AppColor.primary,
unselectedLabelColor: AppColor.textSecondary,
indicatorColor: AppColor.primary,
indicatorWeight: 3,
labelStyle: AppStyle.md.copyWith(fontWeight: FontWeight.w600),
tabAlignment: TabAlignment.start,
unselectedLabelStyle: AppStyle.md,
tabs: const [
Tab(text: 'Semua'),
Tab(text: 'Menunggu'),
Tab(text: 'Diproses'),
Tab(text: 'Selesai'),
Tab(text: 'Dibatalkan'),
],
),
);
}
Widget _buildOrderContent() {
if (_orders.isEmpty) {
return _buildEmptyState();
}
return TabBarView(
controller: _tabController,
children: [
_buildOrderList(_orders),
_buildOrderList(
_orders.where((o) => o.status == OrderStatus.pending).toList(),
),
_buildOrderList(
_orders.where((o) => o.status == OrderStatus.processing).toList(),
),
_buildOrderList(
_orders.where((o) => o.status == OrderStatus.completed).toList(),
),
_buildOrderList(
_orders.where((o) => o.status == OrderStatus.cancelled).toList(),
),
],
);
}
Widget _buildOrderList(List<Order> orders) {
if (orders.isEmpty) {
return _buildEmptyState();
}
return RefreshIndicator(
onRefresh: _refreshOrders,
color: AppColor.primary,
child: ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: orders.length,
itemBuilder: (context, index) {
return OrderCard(order: orders[index]);
},
),
);
}
Widget _buildEmptyState() {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
width: 120,
height: 120,
decoration: BoxDecoration(
color: AppColor.primary.withOpacity(0.1),
borderRadius: BorderRadius.circular(60),
),
child: Icon(
Icons.receipt_long,
size: 60,
color: AppColor.primary.withOpacity(0.5),
),
),
const SizedBox(height: 24),
Text(
'Belum Ada Pesanan',
style: AppStyle.h6.copyWith(
fontWeight: FontWeight.bold,
color: AppColor.textPrimary,
),
),
const SizedBox(height: 8),
Text(
'Pesanan akan muncul di sini setelah\npelanggan mulai memesan.',
style: AppStyle.md.copyWith(color: AppColor.textSecondary),
textAlign: TextAlign.center,
),
const SizedBox(height: 32),
ElevatedButton.icon(
onPressed: _refreshOrders,
icon: const Icon(Icons.refresh, size: 20),
label: const Text('Muat Ulang'),
style: ElevatedButton.styleFrom(
backgroundColor: AppColor.primary,
foregroundColor: AppColor.white,
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
),
],
),
);
}
Widget _buildLoadingState() {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(AppColor.primary),
),
const SizedBox(height: 16),
Text(
'Memuat pesanan...',
style: AppStyle.md.copyWith(color: AppColor.textSecondary),
),
],
),
);
}
void _showFilterDialog() {
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text(
'Filter Pesanan',
style: AppStyle.lg.copyWith(fontWeight: FontWeight.bold),
),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Add filter options here
Text('Opsi filter akan segera hadir...', style: AppStyle.md),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(
'Tutup',
style: AppStyle.md.copyWith(color: AppColor.primary),
),
),
],
);
},
);
}
Future<void> _refreshOrders() async {
setState(() {
_isLoading = true;
});
await Future.delayed(const Duration(seconds: 1));
setState(() {
_isLoading = false;
// Uncomment untuk testing dengan data
_orders = _generateSampleOrders();
});
}
}

View File

@ -0,0 +1,332 @@
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import '../../../../../../common/theme/theme.dart';
import '../order_page.dart';
class OrderCard extends StatelessWidget {
final Order order;
const OrderCard({super.key, required this.order});
@override
Widget build(BuildContext context) {
return Container(
margin: const EdgeInsets.only(bottom: 16),
decoration: BoxDecoration(
color: AppColor.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: AppColor.black.withOpacity(0.06),
blurRadius: 16,
offset: const Offset(0, 3),
),
],
),
child: InkWell(
onTap: () => _showOrderDetail(order),
borderRadius: BorderRadius.circular(16),
child: Padding(
padding: const EdgeInsets.all(18),
child: Column(
children: [
_buildHeader(),
const SizedBox(height: 16),
_buildContent(),
const SizedBox(height: 16),
_buildFooter(),
],
),
),
),
);
}
Widget _buildHeader() {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 4,
),
decoration: BoxDecoration(
color: AppColor.primary.withOpacity(0.1),
borderRadius: BorderRadius.circular(6),
),
child: Text(
order.id,
style: AppStyle.sm.copyWith(
fontWeight: FontWeight.w600,
color: AppColor.primary,
),
),
),
],
),
const SizedBox(height: 6),
Text(
DateFormat('dd MMM yyyy • HH:mm').format(order.orderDate),
style: AppStyle.sm.copyWith(color: AppColor.textSecondary),
),
],
),
),
_buildStatusChip(),
],
);
}
Widget _buildStatusChip() {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
decoration: BoxDecoration(
color: _getStatusColor().withOpacity(0.1),
borderRadius: BorderRadius.circular(20),
border: Border.all(color: _getStatusColor().withOpacity(0.2), width: 1),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(_getStatusIcon(), size: 12, color: _getStatusColor()),
const SizedBox(width: 6),
Text(
_getStatusText(),
style: AppStyle.sm.copyWith(
color: _getStatusColor(),
fontWeight: FontWeight.w600,
),
),
],
),
);
}
Widget _buildContent() {
return Container(
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: AppColor.background,
borderRadius: BorderRadius.circular(12),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
Icons.restaurant_menu_outlined,
size: 16,
color: AppColor.textSecondary,
),
const SizedBox(width: 6),
Text(
'${order.items.length} item pesanan',
style: AppStyle.sm.copyWith(
fontWeight: FontWeight.w500,
color: AppColor.textSecondary,
),
),
],
),
const SizedBox(height: 10),
...order.items
.take(3)
.map(
(item) => Container(
margin: const EdgeInsets.only(bottom: 6),
child: Row(
children: [
Container(
width: 20,
height: 20,
decoration: BoxDecoration(
color: AppColor.primary.withOpacity(0.1),
borderRadius: BorderRadius.circular(10),
),
child: Center(
child: Text(
'${item.quantity}',
style: AppStyle.xs.copyWith(
fontWeight: FontWeight.w600,
color: AppColor.primary,
),
),
),
),
const SizedBox(width: 10),
Expanded(
child: Text(
item.name,
style: AppStyle.sm.copyWith(
color: AppColor.textPrimary,
fontWeight: FontWeight.w500,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
Text(
'Rp ${_formatCurrency(item.price * item.quantity)}',
style: AppStyle.sm.copyWith(
color: AppColor.textPrimary,
fontWeight: FontWeight.w600,
),
),
],
),
),
),
if (order.items.length > 3) ...[
Container(
margin: const EdgeInsets.only(top: 4),
child: Text(
'+${order.items.length - 3} item lainnya',
style: AppStyle.xs.copyWith(
color: AppColor.textSecondary,
fontStyle: FontStyle.italic,
),
),
),
],
if (order.notes != null) ...[
const SizedBox(height: 10),
Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: AppColor.warning.withOpacity(0.05),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: AppColor.warning.withOpacity(0.2)),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(
Icons.sticky_note_2_outlined,
size: 14,
color: AppColor.warning,
),
const SizedBox(width: 6),
Expanded(
child: Text(
order.notes!,
style: AppStyle.xs.copyWith(
color: AppColor.textPrimary,
height: 1.3,
),
),
),
],
),
),
],
],
),
);
}
Widget _buildFooter() {
return Column(
children: [
Container(
height: 1,
width: double.infinity,
color: AppColor.border.withOpacity(0.3),
),
const SizedBox(height: 12),
Row(
children: [
Icon(
order.address != null
? Icons.location_on_outlined
: Icons.store_outlined,
size: 16,
color: AppColor.textSecondary,
),
const SizedBox(width: 6),
Expanded(
child: Text(
order.address ?? 'Ambil di tempat',
style: AppStyle.sm.copyWith(color: AppColor.textSecondary),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
const SizedBox(width: 12),
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
'Total',
style: AppStyle.xs.copyWith(color: AppColor.textSecondary),
),
Text(
'Rp ${_formatCurrency(order.totalAmount)}',
style: AppStyle.lg.copyWith(
fontWeight: FontWeight.w700,
color: AppColor.primary,
),
),
],
),
],
),
],
);
}
Color _getStatusColor() {
switch (order.status) {
case OrderStatus.pending:
return AppColor.warning;
case OrderStatus.processing:
return AppColor.info;
case OrderStatus.completed:
return AppColor.success;
case OrderStatus.cancelled:
return AppColor.error;
}
}
String _getStatusText() {
switch (order.status) {
case OrderStatus.pending:
return 'Menunggu';
case OrderStatus.processing:
return 'Diproses';
case OrderStatus.completed:
return 'Selesai';
case OrderStatus.cancelled:
return 'Dibatalkan';
}
}
IconData _getStatusIcon() {
switch (order.status) {
case OrderStatus.pending:
return Icons.schedule;
case OrderStatus.processing:
return Icons.hourglass_empty;
case OrderStatus.completed:
return Icons.check_circle;
case OrderStatus.cancelled:
return Icons.cancel;
}
}
String _formatCurrency(double amount) {
final formatter = NumberFormat('#,###');
return formatter.format(amount);
}
void _showOrderDetail(Order order) {
// Implementation for showing order details
}
}

View File

@ -0,0 +1,548 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:url_launcher/url_launcher.dart';
import '../../../../../common/theme/theme.dart';
@RoutePage()
class ProfilePage extends StatelessWidget {
const ProfilePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppColor.background,
appBar: AppBar(title: Text('Akun')),
body: SingleChildScrollView(
child: Column(
children: [
// Profile Header
Container(
width: double.infinity,
color: AppColor.white,
padding: const EdgeInsets.all(20),
child: Column(
children: [
// Profile Avatar & Info
Container(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: AppColor.primaryGradient,
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(16),
),
child: Stack(
children: [
// Background Pattern
Positioned(
top: -20,
right: -20,
child: Container(
width: 80,
height: 80,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: AppColor.white.withOpacity(0.1),
),
),
),
Positioned(
top: 30,
right: 20,
child: Container(
width: 40,
height: 40,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: AppColor.white.withOpacity(0.08),
),
),
),
Positioned(
bottom: -10,
left: -10,
child: Container(
width: 60,
height: 60,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: AppColor.white.withOpacity(0.06),
),
),
),
// Decorative Lines
Positioned(
top: 10,
left: -5,
child: Transform.rotate(
angle: 0.5,
child: Container(
width: 30,
height: 2,
decoration: BoxDecoration(
color: AppColor.white.withOpacity(0.15),
borderRadius: BorderRadius.circular(1),
),
),
),
),
Positioned(
bottom: 15,
right: 10,
child: Transform.rotate(
angle: -0.5,
child: Container(
width: 25,
height: 2,
decoration: BoxDecoration(
color: AppColor.white.withOpacity(0.15),
borderRadius: BorderRadius.circular(1),
),
),
),
),
// Main Content
Padding(
padding: const EdgeInsets.all(16.0),
child: Row(
children: [
// Avatar
Container(
width: 60,
height: 60,
decoration: BoxDecoration(
color: AppColor.white,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: AppColor.black.withOpacity(0.1),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: Icon(
Icons.person,
size: 30,
color: AppColor.primary,
),
),
const SizedBox(width: 16),
// User Info
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'EFRIL',
style: AppStyle.lg.copyWith(
fontWeight: FontWeight.bold,
color: AppColor.white,
letterSpacing: 0.5,
),
),
const SizedBox(height: 4),
Text(
'+6283873987851',
style: AppStyle.sm.copyWith(
color: AppColor.white.withOpacity(0.9),
),
),
],
),
),
// Arrow Icon
Icon(
Icons.arrow_forward_ios,
color: AppColor.white,
size: 14,
),
],
),
),
],
),
),
const SizedBox(height: 20),
// Share the Sip Card
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppColor.backgroundLight,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: AppColor.borderLight),
),
child: Row(
children: [
// Share Icon
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: AppColor.success.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Icon(
Icons.share,
color: AppColor.success,
size: 20,
),
),
const SizedBox(width: 12),
// Share Info
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Share the Sip',
style: AppStyle.md.copyWith(
fontWeight: FontWeight.w600,
color: AppColor.textPrimary,
),
),
Text(
'Bagikan kode referral, dapatkan hadiah',
style: AppStyle.sm.copyWith(
color: AppColor.textSecondary,
),
),
],
),
),
Icon(
Icons.arrow_forward_ios,
color: AppColor.textSecondary,
size: 14,
),
],
),
),
],
),
),
const SizedBox(height: 8),
// Menu Items
Container(
color: AppColor.white,
child: Column(
children: [
_buildMenuItem(
icon: Icons.location_on_outlined,
title: 'Alamat Tersimpan',
onTap: () {},
),
_buildMenuItem(
icon: Icons.payment_outlined,
title: 'Pembayaran',
onTap: () {},
),
_buildMenuItem(
icon: Icons.help_outline,
title: 'Pusat Bantuan',
onTap: () {},
),
_buildMenuItem(
icon: Icons.settings_outlined,
title: 'Pengaturan',
onTap: () {},
showDivider: false,
),
],
),
),
const SizedBox(height: 8),
// Legal & Privacy Section
Container(
color: AppColor.white,
child: Column(
children: [
_buildMenuItem(
icon: Icons.description_outlined,
title: 'Syarat dan Ketentuan',
onTap: () {},
),
_buildMenuItem(
icon: Icons.privacy_tip_outlined,
title: 'Kebijakan Privasi',
onTap: () {},
showDivider: false,
),
],
),
),
const SizedBox(height: 8),
// Social Media Section
Container(
color: AppColor.white,
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Media Sosial',
style: AppStyle.md.copyWith(
fontWeight: FontWeight.w600,
color: AppColor.textPrimary,
),
),
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
_buildSocialButton(
icon: Icons.camera_alt,
color: Colors.purple,
onTap: () => _launchSocialMedia('instagram'),
),
_buildSocialButton(
icon: Icons.facebook,
color: Colors.blue,
onTap: () => _launchSocialMedia('facebook'),
),
_buildSocialButton(
icon: Icons.play_arrow,
color: Colors.red,
onTap: () => _launchSocialMedia('youtube'),
),
_buildSocialButton(
icon: Icons.close,
color: Colors.black,
onTap: () => _launchSocialMedia('twitter'),
),
],
),
],
),
),
const SizedBox(height: 8),
// Customer Service Section
Container(
color: AppColor.white,
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Butuh Bantuan?',
style: AppStyle.lg.copyWith(
fontWeight: FontWeight.w600,
color: AppColor.textPrimary,
),
),
const SizedBox(height: 8),
Text(
'Customer Service kami siap untuk membantu',
style: AppStyle.sm.copyWith(color: AppColor.textSecondary),
),
const SizedBox(height: 16),
// WhatsApp Customer Service
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppColor.backgroundLight,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: AppColor.borderLight),
),
child: Row(
children: [
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: Colors.green,
borderRadius: BorderRadius.circular(8),
),
child: Icon(
Icons.chat,
color: AppColor.white,
size: 20,
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Enaklo Customer Service (chat only)',
style: AppStyle.sm.copyWith(
color: AppColor.textSecondary,
),
),
Text(
'0812-1111-8456',
style: AppStyle.md.copyWith(
fontWeight: FontWeight.w600,
color: AppColor.success,
),
),
],
),
),
Icon(
Icons.arrow_forward_ios,
color: AppColor.textSecondary,
size: 14,
),
],
),
),
],
),
),
const SizedBox(height: 20),
// Footer Section
Container(
color: AppColor.white,
padding: const EdgeInsets.all(20),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Version 4.6.1',
style: AppStyle.sm.copyWith(color: AppColor.textSecondary),
),
GestureDetector(
onTap: () => _showLogoutDialog(context),
child: Text(
'Logout',
style: AppStyle.sm.copyWith(
color: AppColor.primary,
fontWeight: FontWeight.w600,
),
),
),
],
),
),
const SizedBox(height: 100), // Bottom spacing
],
),
),
);
}
Widget _buildMenuItem({
required IconData icon,
required String title,
required VoidCallback onTap,
bool showDivider = true,
}) {
return Column(
children: [
ListTile(
leading: Icon(icon, color: AppColor.textSecondary, size: 24),
title: Text(
title,
style: AppStyle.md.copyWith(color: AppColor.textPrimary),
),
trailing: Icon(
Icons.arrow_forward_ios,
color: AppColor.textSecondary,
size: 16,
),
onTap: onTap,
contentPadding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 4,
),
),
if (showDivider)
Divider(height: 1, color: AppColor.borderLight, indent: 60),
],
);
}
Widget _buildSocialButton({
required IconData icon,
required Color color,
required VoidCallback onTap,
}) {
return GestureDetector(
onTap: onTap,
child: Container(
width: 48,
height: 48,
decoration: BoxDecoration(color: color, shape: BoxShape.circle),
child: Icon(icon, color: AppColor.white, size: 24),
),
);
}
void _launchSocialMedia(String platform) async {
String url = '';
switch (platform) {
case 'instagram':
url = 'https://instagram.com/';
break;
case 'facebook':
url = 'https://facebook.com/';
break;
case 'youtube':
url = 'https://youtube.com/';
break;
case 'twitter':
url = 'https://twitter.com/';
break;
}
if (await canLaunch(url)) {
await launch(url);
}
}
void _showLogoutDialog(BuildContext context) {
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text(
'Logout',
style: AppStyle.lg.copyWith(fontWeight: FontWeight.w600),
),
content: Text(
'Apakah Anda yakin ingin keluar dari aplikasi?',
style: AppStyle.md,
),
actions: [
TextButton(
child: Text(
'Batal',
style: AppStyle.md.copyWith(color: AppColor.textSecondary),
),
onPressed: () => Navigator.of(context).pop(),
),
TextButton(
child: Text(
'Logout',
style: AppStyle.md.copyWith(
color: AppColor.primary,
fontWeight: FontWeight.w600,
),
),
onPressed: () {
Navigator.of(context).pop();
// Add logout logic here
// Example: context.router.pushAndClearStack('/login');
},
),
],
);
},
);
}
}

View File

@ -0,0 +1,132 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import '../../../../../common/theme/theme.dart';
import 'widgets/voucher_card.dart';
@RoutePage()
class VoucherPage extends StatelessWidget {
const VoucherPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Voucher'),
automaticallyImplyLeading: false,
bottom: PreferredSize(
preferredSize: Size.fromHeight(70),
child: Container(
margin: EdgeInsets.fromLTRB(16, 0, 16, 16),
decoration: BoxDecoration(
color: AppColor.white,
borderRadius: BorderRadius.circular(25),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: Offset(0, 2),
),
],
),
child: TextField(
cursorColor: AppColor.primary,
decoration: InputDecoration(
hintText: 'Punya kode promo? Masukkan disini',
hintStyle: TextStyle(color: AppColor.textLight, fontSize: 14),
disabledBorder: InputBorder.none,
enabledBorder: InputBorder.none,
errorBorder: InputBorder.none,
focusedBorder: InputBorder.none,
prefixIcon: Container(
margin: EdgeInsets.all(12),
width: 24,
height: 24,
decoration: BoxDecoration(
color: AppColor.primary,
borderRadius: BorderRadius.circular(6),
),
child: Icon(
Icons.local_offer,
color: AppColor.white,
size: 14,
),
),
border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(
horizontal: 16,
vertical: 16,
),
),
),
),
),
),
body: Column(
children: [
// Voucher Belanja Section
Expanded(
child: Column(
children: [
// Section Header
Padding(
padding: EdgeInsets.all(16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Voucher Belanja',
style: TextStyle(
color: AppColor.textPrimary,
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
Text(
'10 voucher',
style: TextStyle(
color: AppColor.textSecondary,
fontSize: 14,
),
),
],
),
),
// Voucher List
Expanded(
child: ListView(
padding: EdgeInsets.symmetric(horizontal: 16),
children: [
VoucherCard(
title: 'New User Voucher - Diskon 50% hingga Rp35K',
subtitle: 'Tanpa Min. Belanja',
expireDate: '25 Sep 2025',
minTransaction: '-',
),
SizedBox(height: 16),
VoucherCard(
title: 'New User Voucher - Diskon 35% hingga Rp50K',
subtitle: 'Tanpa Min. Belanja',
expireDate: '25 Sep 2025',
minTransaction: '-',
),
SizedBox(height: 16),
VoucherCard(
title: 'New User Voucher - Diskon 25% hingga Rp50K',
subtitle: 'Tanpa Min. Belanja',
expireDate: '25 Sep 2025',
minTransaction: '-',
),
SizedBox(height: 16),
],
),
),
],
),
),
],
),
);
}
}

View File

@ -0,0 +1,209 @@
import 'package:flutter/material.dart';
import '../../../../../../common/theme/theme.dart';
import '../../../../../../common/ui/clipper/voucher_clipper.dart';
import '../../../../../../common/ui/painter/dashed_line_painter.dart';
class VoucherCard extends StatelessWidget {
final String title;
final String subtitle;
final String expireDate;
final String minTransaction;
const VoucherCard({
super.key,
required this.title,
required this.subtitle,
required this.expireDate,
required this.minTransaction,
});
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.08),
blurRadius: 12,
offset: Offset(0, 4),
spreadRadius: 0,
),
BoxShadow(
color: Colors.black.withOpacity(0.04),
blurRadius: 6,
offset: Offset(0, 2),
spreadRadius: 0,
),
],
),
child: ClipPath(
clipper: VoucherClipper(),
child: Container(
decoration: BoxDecoration(color: Colors.white),
child: Column(
children: [
// Main Content
Padding(
padding: EdgeInsets.all(16),
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: AppStyle.lg.copyWith(
color: AppColor.textPrimary,
fontWeight: FontWeight.w600,
),
),
SizedBox(height: 4),
Text(
subtitle,
style: AppStyle.md.copyWith(
color: AppColor.textSecondary,
),
),
],
),
),
SizedBox(width: 12),
// Voucher Icon
Container(
width: 50,
height: 50,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.grey[300]!, width: 1),
),
child: Stack(
alignment: Alignment.center,
children: [
Icon(
Icons.local_offer_outlined,
color: AppColor.primary,
size: 24,
),
Positioned(
top: 6,
right: 6,
child: Container(
width: 16,
height: 16,
decoration: BoxDecoration(
color: AppColor.error,
shape: BoxShape.circle,
),
child: Icon(
Icons.percent,
color: Colors.white,
size: 10,
),
),
),
],
),
),
],
),
),
// Dashed line divider
Container(
height: 1,
margin: EdgeInsets.symmetric(horizontal: 20),
child: CustomPaint(
size: Size(double.infinity, 1),
painter: DashedLinePainter(),
),
),
// Bottom Section
Padding(
padding: EdgeInsets.all(16),
child: Row(
children: [
Expanded(
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Berlaku Hingga',
style: AppStyle.xs.copyWith(
color: AppColor.textSecondary,
fontSize: 10,
),
),
SizedBox(height: 2),
Text(
expireDate,
style: TextStyle(
color: AppColor.textPrimary,
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
],
),
),
SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Min Transaksi',
style: AppStyle.xs.copyWith(
color: AppColor.textSecondary,
fontSize: 10,
),
),
SizedBox(height: 2),
Text(
minTransaction,
style: AppStyle.md.copyWith(
color: AppColor.textPrimary,
fontWeight: FontWeight.w500,
),
),
],
),
),
],
),
),
SizedBox(width: 16),
// Pakai Button
Container(
padding: EdgeInsets.symmetric(
horizontal: 24,
vertical: 10,
),
decoration: BoxDecoration(
color: AppColor.primary,
borderRadius: BorderRadius.circular(20),
),
child: Text(
'Pakai',
style: TextStyle(
color: AppColor.white,
fontSize: 14,
fontWeight: FontWeight.w600,
),
),
),
],
),
),
],
),
),
),
);
}
}

View File

@ -0,0 +1,81 @@
import 'package:flutter/material.dart';
import '../../../../../../common/theme/theme.dart';
class VoucherEmptyCard extends StatelessWidget {
const VoucherEmptyCard({super.key});
@override
Widget build(BuildContext context) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Empty State Icon
Container(
width: 120,
height: 120,
decoration: BoxDecoration(
color: AppColor.primary.withOpacity(0.1),
shape: BoxShape.circle,
),
child: Icon(
Icons.local_offer_outlined,
size: 60,
color: AppColor.primary.withOpacity(0.5),
),
),
SizedBox(height: 24),
// Empty State Title
Text(
'Belum Ada Voucher',
style: TextStyle(
color: AppColor.textPrimary,
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
SizedBox(height: 8),
// Empty State Description
Padding(
padding: EdgeInsets.symmetric(horizontal: 32),
child: Text(
'Kamu belum memiliki voucher saat ini.\nCoba masukkan kode promo di atas atau\ncari promo menarik lainnya.',
textAlign: TextAlign.center,
style: TextStyle(
color: AppColor.textSecondary,
fontSize: 14,
height: 1.5,
),
),
),
SizedBox(height: 32),
// Explore Button
ElevatedButton(
onPressed: () {
// Navigate to explore or promo page
},
style: ElevatedButton.styleFrom(
backgroundColor: AppColor.primary,
foregroundColor: AppColor.white,
padding: EdgeInsets.symmetric(horizontal: 24, vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(25),
),
),
child: Text(
'Jelajahi Promo',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
),
),
],
),
);
}
}

View File

@ -0,0 +1,39 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
class MainBottomNavbar extends StatelessWidget {
final TabsRouter tabsRouter;
const MainBottomNavbar({super.key, required this.tabsRouter});
@override
Widget build(BuildContext context) {
return BottomNavigationBar(
currentIndex: tabsRouter.activeIndex,
onTap: (index) {
tabsRouter.setActiveIndex(index);
},
items: const [
BottomNavigationBarItem(
icon: Icon(Icons.home),
label: 'Home',
tooltip: 'Home',
),
BottomNavigationBarItem(
icon: Icon(Icons.discount),
label: 'Voucher',
tooltip: 'Voucher',
),
BottomNavigationBarItem(
icon: Icon(Icons.list),
label: 'Pesanan',
tooltip: 'Pesanan',
),
BottomNavigationBarItem(
icon: Icon(Icons.person),
label: 'Profil',
tooltip: 'Profil',
),
],
);
}
}

View File

@ -15,5 +15,17 @@ class AppRouter extends RootStackRouter {
AutoRoute(page: LoginRoute.page),
AutoRoute(page: RegisterRoute.page),
AutoRoute(page: OtpRoute.page),
AutoRoute(page: PinRoute.page),
// Main
AutoRoute(
page: MainRoute.page,
children: [
AutoRoute(page: HomeRoute.page),
AutoRoute(page: VoucherRoute.page),
AutoRoute(page: OrderRoute.page),
AutoRoute(page: ProfileRoute.page),
],
),
];
}

View File

@ -9,91 +9,228 @@
// coverage:ignore-file
// ignore_for_file: no_leading_underscores_for_library_prefixes
import 'package:auto_route/auto_route.dart' as _i6;
import 'package:enaklo/presentation/pages/auth/login/login_page.dart' as _i1;
import 'package:enaklo/presentation/pages/auth/otp/otp_page.dart' as _i3;
import 'package:auto_route/auto_route.dart' as _i12;
import 'package:enaklo/presentation/pages/auth/login/login_page.dart' as _i2;
import 'package:enaklo/presentation/pages/auth/otp/otp_page.dart' as _i6;
import 'package:enaklo/presentation/pages/auth/pin/pin_page.dart' as _i7;
import 'package:enaklo/presentation/pages/auth/register/register_page.dart'
as _i4;
as _i9;
import 'package:enaklo/presentation/pages/main/main_page.dart' as _i3;
import 'package:enaklo/presentation/pages/main/pages/home/home_page.dart'
as _i1;
import 'package:enaklo/presentation/pages/main/pages/order/order_page.dart'
as _i5;
import 'package:enaklo/presentation/pages/main/pages/profile/profile_page.dart'
as _i8;
import 'package:enaklo/presentation/pages/main/pages/voucher/voucher_page.dart'
as _i11;
import 'package:enaklo/presentation/pages/onboarding/onboarding_page.dart'
as _i2;
import 'package:enaklo/presentation/pages/splash/splash_page.dart' as _i5;
as _i4;
import 'package:enaklo/presentation/pages/splash/splash_page.dart' as _i10;
import 'package:flutter/material.dart' as _i13;
/// generated route for
/// [_i1.LoginPage]
class LoginRoute extends _i6.PageRouteInfo<void> {
const LoginRoute({List<_i6.PageRouteInfo>? children})
/// [_i1.HomePage]
class HomeRoute extends _i12.PageRouteInfo<void> {
const HomeRoute({List<_i12.PageRouteInfo>? children})
: super(HomeRoute.name, initialChildren: children);
static const String name = 'HomeRoute';
static _i12.PageInfo page = _i12.PageInfo(
name,
builder: (data) {
return const _i1.HomePage();
},
);
}
/// generated route for
/// [_i2.LoginPage]
class LoginRoute extends _i12.PageRouteInfo<void> {
const LoginRoute({List<_i12.PageRouteInfo>? children})
: super(LoginRoute.name, initialChildren: children);
static const String name = 'LoginRoute';
static _i6.PageInfo page = _i6.PageInfo(
static _i12.PageInfo page = _i12.PageInfo(
name,
builder: (data) {
return const _i1.LoginPage();
return const _i2.LoginPage();
},
);
}
/// generated route for
/// [_i2.OnboardingPage]
class OnboardingRoute extends _i6.PageRouteInfo<void> {
const OnboardingRoute({List<_i6.PageRouteInfo>? children})
/// [_i3.MainPage]
class MainRoute extends _i12.PageRouteInfo<void> {
const MainRoute({List<_i12.PageRouteInfo>? children})
: super(MainRoute.name, initialChildren: children);
static const String name = 'MainRoute';
static _i12.PageInfo page = _i12.PageInfo(
name,
builder: (data) {
return const _i3.MainPage();
},
);
}
/// generated route for
/// [_i4.OnboardingPage]
class OnboardingRoute extends _i12.PageRouteInfo<void> {
const OnboardingRoute({List<_i12.PageRouteInfo>? children})
: super(OnboardingRoute.name, initialChildren: children);
static const String name = 'OnboardingRoute';
static _i6.PageInfo page = _i6.PageInfo(
static _i12.PageInfo page = _i12.PageInfo(
name,
builder: (data) {
return const _i2.OnboardingPage();
return const _i4.OnboardingPage();
},
);
}
/// generated route for
/// [_i3.OtpPage]
class OtpRoute extends _i6.PageRouteInfo<void> {
const OtpRoute({List<_i6.PageRouteInfo>? children})
/// [_i5.OrderPage]
class OrderRoute extends _i12.PageRouteInfo<void> {
const OrderRoute({List<_i12.PageRouteInfo>? children})
: super(OrderRoute.name, initialChildren: children);
static const String name = 'OrderRoute';
static _i12.PageInfo page = _i12.PageInfo(
name,
builder: (data) {
return const _i5.OrderPage();
},
);
}
/// generated route for
/// [_i6.OtpPage]
class OtpRoute extends _i12.PageRouteInfo<void> {
const OtpRoute({List<_i12.PageRouteInfo>? children})
: super(OtpRoute.name, initialChildren: children);
static const String name = 'OtpRoute';
static _i6.PageInfo page = _i6.PageInfo(
static _i12.PageInfo page = _i12.PageInfo(
name,
builder: (data) {
return const _i3.OtpPage();
return const _i6.OtpPage();
},
);
}
/// generated route for
/// [_i4.RegisterPage]
class RegisterRoute extends _i6.PageRouteInfo<void> {
const RegisterRoute({List<_i6.PageRouteInfo>? children})
/// [_i7.PinPage]
class PinRoute extends _i12.PageRouteInfo<PinRouteArgs> {
PinRoute({
_i13.Key? key,
bool isCreatePin = true,
String? title,
List<_i12.PageRouteInfo>? children,
}) : super(
PinRoute.name,
args: PinRouteArgs(key: key, isCreatePin: isCreatePin, title: title),
initialChildren: children,
);
static const String name = 'PinRoute';
static _i12.PageInfo page = _i12.PageInfo(
name,
builder: (data) {
final args = data.argsAs<PinRouteArgs>(
orElse: () => const PinRouteArgs(),
);
return _i7.PinPage(
key: args.key,
isCreatePin: args.isCreatePin,
title: args.title,
);
},
);
}
class PinRouteArgs {
const PinRouteArgs({this.key, this.isCreatePin = true, this.title});
final _i13.Key? key;
final bool isCreatePin;
final String? title;
@override
String toString() {
return 'PinRouteArgs{key: $key, isCreatePin: $isCreatePin, title: $title}';
}
}
/// generated route for
/// [_i8.ProfilePage]
class ProfileRoute extends _i12.PageRouteInfo<void> {
const ProfileRoute({List<_i12.PageRouteInfo>? children})
: super(ProfileRoute.name, initialChildren: children);
static const String name = 'ProfileRoute';
static _i12.PageInfo page = _i12.PageInfo(
name,
builder: (data) {
return const _i8.ProfilePage();
},
);
}
/// generated route for
/// [_i9.RegisterPage]
class RegisterRoute extends _i12.PageRouteInfo<void> {
const RegisterRoute({List<_i12.PageRouteInfo>? children})
: super(RegisterRoute.name, initialChildren: children);
static const String name = 'RegisterRoute';
static _i6.PageInfo page = _i6.PageInfo(
static _i12.PageInfo page = _i12.PageInfo(
name,
builder: (data) {
return const _i4.RegisterPage();
return const _i9.RegisterPage();
},
);
}
/// generated route for
/// [_i5.SplashPage]
class SplashRoute extends _i6.PageRouteInfo<void> {
const SplashRoute({List<_i6.PageRouteInfo>? children})
/// [_i10.SplashPage]
class SplashRoute extends _i12.PageRouteInfo<void> {
const SplashRoute({List<_i12.PageRouteInfo>? children})
: super(SplashRoute.name, initialChildren: children);
static const String name = 'SplashRoute';
static _i6.PageInfo page = _i6.PageInfo(
static _i12.PageInfo page = _i12.PageInfo(
name,
builder: (data) {
return const _i5.SplashPage();
return const _i10.SplashPage();
},
);
}
/// generated route for
/// [_i11.VoucherPage]
class VoucherRoute extends _i12.PageRouteInfo<void> {
const VoucherRoute({List<_i12.PageRouteInfo>? children})
: super(VoucherRoute.name, initialChildren: children);
static const String name = 'VoucherRoute';
static _i12.PageInfo page = _i12.PageInfo(
name,
builder: (data) {
return const _i11.VoucherPage();
},
);
}

View File

@ -6,6 +6,10 @@
#include "generated_plugin_registrant.h"
#include <url_launcher_linux/url_launcher_plugin.h>
void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
}

View File

@ -3,6 +3,7 @@
#
list(APPEND FLUTTER_PLUGIN_LIST
url_launcher_linux
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST

View File

@ -8,9 +8,11 @@ import Foundation
import connectivity_plus
import path_provider_foundation
import shared_preferences_foundation
import url_launcher_macos
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
}

View File

@ -933,6 +933,70 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.4.0"
url_launcher:
dependency: "direct main"
description:
name: url_launcher
sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8
url: "https://pub.dev"
source: hosted
version: "6.3.2"
url_launcher_android:
dependency: transitive
description:
name: url_launcher_android
sha256: "69ee86740f2847b9a4ba6cffa74ed12ce500bbe2b07f3dc1e643439da60637b7"
url: "https://pub.dev"
source: hosted
version: "6.3.18"
url_launcher_ios:
dependency: transitive
description:
name: url_launcher_ios
sha256: d80b3f567a617cb923546034cc94bfe44eb15f989fe670b37f26abdb9d939cb7
url: "https://pub.dev"
source: hosted
version: "6.3.4"
url_launcher_linux:
dependency: transitive
description:
name: url_launcher_linux
sha256: "4e9ba368772369e3e08f231d2301b4ef72b9ff87c31192ef471b380ef29a4935"
url: "https://pub.dev"
source: hosted
version: "3.2.1"
url_launcher_macos:
dependency: transitive
description:
name: url_launcher_macos
sha256: c043a77d6600ac9c38300567f33ef12b0ef4f4783a2c1f00231d2b1941fea13f
url: "https://pub.dev"
source: hosted
version: "3.2.3"
url_launcher_platform_interface:
dependency: transitive
description:
name: url_launcher_platform_interface
sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029"
url: "https://pub.dev"
source: hosted
version: "2.3.2"
url_launcher_web:
dependency: transitive
description:
name: url_launcher_web
sha256: "4bd2b7b4dc4d4d0b94e5babfffbca8eac1a126c7f3d6ecbc1a11013faa3abba2"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
url_launcher_windows:
dependency: transitive
description:
name: url_launcher_windows
sha256: "3284b6d2ac454cf34f114e1d3319866fdd1e19cdc329999057e44ffe936cfa77"
url: "https://pub.dev"
source: hosted
version: "3.1.4"
vector_graphics:
dependency: transitive
description:

View File

@ -28,6 +28,7 @@ dependencies:
json_annotation: ^4.9.0
shared_preferences: ^2.5.3
carousel_slider: ^5.1.1
url_launcher: ^6.3.2
dev_dependencies:
flutter_test:

View File

@ -7,8 +7,11 @@
#include "generated_plugin_registrant.h"
#include <connectivity_plus/connectivity_plus_windows_plugin.h>
#include <url_launcher_windows/url_launcher_windows.h>
void RegisterPlugins(flutter::PluginRegistry* registry) {
ConnectivityPlusWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin"));
UrlLauncherWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
}

View File

@ -4,6 +4,7 @@
list(APPEND FLUTTER_PLUGIN_LIST
connectivity_plus
url_launcher_windows
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST