feat: voucher card
This commit is contained in:
parent
8312429be3
commit
8412220a06
@ -26,6 +26,7 @@ class ThemeApp {
|
||||
),
|
||||
centerTitle: true,
|
||||
iconTheme: IconThemeData(color: AppColor.primary),
|
||||
scrolledUnderElevation: 0.0,
|
||||
),
|
||||
elevatedButtonTheme: ElevatedButtonThemeData(
|
||||
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: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 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',
|
||||
),
|
||||
BottomNavigationBarItem(
|
||||
icon: Icon(Icons.card_giftcard),
|
||||
icon: Icon(Icons.discount),
|
||||
label: 'Voucher',
|
||||
tooltip: 'Voucher',
|
||||
),
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user