feat: voucher card
This commit is contained in:
parent
8312429be3
commit
8412220a06
@ -26,6 +26,7 @@ class ThemeApp {
|
|||||||
),
|
),
|
||||||
centerTitle: true,
|
centerTitle: true,
|
||||||
iconTheme: IconThemeData(color: AppColor.primary),
|
iconTheme: IconThemeData(color: AppColor.primary),
|
||||||
|
scrolledUnderElevation: 0.0,
|
||||||
),
|
),
|
||||||
elevatedButtonTheme: ElevatedButtonThemeData(
|
elevatedButtonTheme: ElevatedButtonThemeData(
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
|
|||||||
61
lib/common/ui/clipper/voucher_clipper.dart
Normal file
61
lib/common/ui/clipper/voucher_clipper.dart
Normal 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;
|
||||||
|
}
|
||||||
27
lib/common/ui/painter/dashed_line_painter.dart
Normal file
27
lib/common/ui/painter/dashed_line_painter.dart
Normal 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;
|
||||||
|
}
|
||||||
@ -1,12 +1,132 @@
|
|||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import '../../../../../common/theme/theme.dart';
|
||||||
|
import 'widgets/voucher_card.dart';
|
||||||
|
|
||||||
@RoutePage()
|
@RoutePage()
|
||||||
class VoucherPage extends StatelessWidget {
|
class VoucherPage extends StatelessWidget {
|
||||||
const VoucherPage({super.key});
|
const VoucherPage({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Center(child: Text('Voucher Page'));
|
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),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -19,7 +19,7 @@ class MainBottomNavbar extends StatelessWidget {
|
|||||||
tooltip: 'Home',
|
tooltip: 'Home',
|
||||||
),
|
),
|
||||||
BottomNavigationBarItem(
|
BottomNavigationBarItem(
|
||||||
icon: Icon(Icons.card_giftcard),
|
icon: Icon(Icons.discount),
|
||||||
label: 'Voucher',
|
label: 'Voucher',
|
||||||
tooltip: 'Voucher',
|
tooltip: 'Voucher',
|
||||||
),
|
),
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user