feat: draw detail page

This commit is contained in:
efrilm
2025-08-29 15:40:45 +07:00
parent 8951ca44eb
commit c36a4059e1
4 changed files with 1270 additions and 306 deletions
+223 -204
View File
@@ -2,6 +2,7 @@ import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import '../../../common/theme/theme.dart';
import '../../router/app_router.gr.dart';
// Models (simplified)
class DrawEvent {
@@ -220,242 +221,260 @@ class _DrawPageState extends State<DrawPage> {
_isUserEntered(draw.id);
final timeRemaining = _getTimeRemaining(draw.drawDate);
return Container(
margin: EdgeInsets.only(bottom: 16),
decoration: BoxDecoration(
color: AppColor.surface,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: AppColor.black.withOpacity(0.08),
blurRadius: 12,
offset: Offset(0, 4),
),
],
),
child: Column(
children: [
// Header with gradient background
Container(
height: 160,
width: double.infinity,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [draw.primaryColor, draw.primaryColor.withOpacity(0.8)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.vertical(top: Radius.circular(12)),
return GestureDetector(
onTap: () => context.router.push(DrawDetailRoute(drawEvent: draw)),
child: Container(
margin: EdgeInsets.only(bottom: 16),
decoration: BoxDecoration(
color: AppColor.surface,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: AppColor.black.withOpacity(0.08),
blurRadius: 12,
offset: Offset(0, 4),
),
child: Stack(
children: [
// Background pattern (coins and gold bar)
Positioned(
right: 20,
top: 20,
child: Opacity(
opacity: 0.3,
child: Column(
children: [
Row(
children: [
_buildCoin(),
SizedBox(width: 8),
_buildCoin(),
],
),
SizedBox(height: 8),
_buildGoldBar(),
],
],
),
child: Column(
children: [
// Header with gradient background
Container(
height: 160,
width: double.infinity,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
draw.primaryColor,
draw.primaryColor.withOpacity(0.8),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.vertical(top: Radius.circular(12)),
),
child: Stack(
children: [
// Background pattern (coins and gold bar)
Positioned(
right: 20,
top: 20,
child: Opacity(
opacity: 0.3,
child: Column(
children: [
Row(
children: [
_buildCoin(),
SizedBox(width: 8),
_buildCoin(),
],
),
SizedBox(height: 8),
_buildGoldBar(),
],
),
),
),
),
// Content
Padding(
padding: EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (draw.isActive)
Container(
padding: EdgeInsets.symmetric(
horizontal: 8,
vertical: 4,
),
decoration: BoxDecoration(
color: AppColor.success,
borderRadius: BorderRadius.circular(4),
),
child: Text(
"AKTIF",
style: AppStyle.xs.copyWith(
color: AppColor.textWhite,
fontWeight: FontWeight.bold,
// Content
Padding(
padding: EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (draw.isActive)
Container(
padding: EdgeInsets.symmetric(
horizontal: 8,
vertical: 4,
),
decoration: BoxDecoration(
color: AppColor.success,
borderRadius: BorderRadius.circular(4),
),
child: Text(
"AKTIF",
style: AppStyle.xs.copyWith(
color: AppColor.textWhite,
fontWeight: FontWeight.bold,
),
),
),
SizedBox(height: 8),
Text(
"MAKAN\nDAPAT",
style: AppStyle.h5.copyWith(
color: AppColor.textWhite,
fontWeight: FontWeight.bold,
height: 0.9,
),
),
SizedBox(height: 8),
Text(
"MAKAN\nDAPAT",
style: AppStyle.h5.copyWith(
color: AppColor.textWhite,
fontWeight: FontWeight.bold,
height: 0.9,
SizedBox(height: 4),
Text(
draw.description,
style: AppStyle.sm.copyWith(
color: AppColor.textWhite,
),
),
),
SizedBox(height: 4),
Text(
draw.description,
style: AppStyle.sm.copyWith(color: AppColor.textWhite),
),
],
],
),
),
),
],
],
),
),
),
// Card content
Padding(
padding: EdgeInsets.all(16),
child: Column(
children: [
// Prize info
Row(
children: [
Text(draw.icon, style: AppStyle.h5),
SizedBox(width: 8),
Text(
draw.prize,
style: AppStyle.lg.copyWith(
fontWeight: FontWeight.bold,
color: AppColor.textPrimary,
),
),
],
),
SizedBox(height: 16),
// Stats row
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
children: [
Text(
"-",
style: AppStyle.h5.copyWith(
fontWeight: FontWeight.bold,
color: AppColor.textPrimary,
),
),
Text(
"Peserta",
style: AppStyle.sm.copyWith(
color: AppColor.textSecondary,
),
),
],
),
Column(
children: [
Text(
"${draw.hadiah}",
style: AppStyle.h5.copyWith(
fontWeight: FontWeight.bold,
color: AppColor.textPrimary,
),
),
Text(
"Hadiah",
style: AppStyle.sm.copyWith(
color: AppColor.textSecondary,
),
),
],
),
Column(
children: [
Text(
"0",
style: AppStyle.h5.copyWith(
fontWeight: FontWeight.bold,
color: AppColor.error,
),
),
Text(
"Voucher Anda",
style: AppStyle.sm.copyWith(
color: AppColor.textSecondary,
),
),
],
),
],
),
SizedBox(height: 16),
// Timer
if (draw.isActive)
// Card content
Padding(
padding: EdgeInsets.all(16),
child: Column(
children: [
// Prize info
Row(
children: [
Icon(Icons.access_time, color: AppColor.error, size: 16),
SizedBox(width: 4),
Text(draw.icon, style: AppStyle.h5),
SizedBox(width: 8),
Text(
"Berakhir dalam:",
style: AppStyle.md.copyWith(
draw.prize,
style: AppStyle.lg.copyWith(
fontWeight: FontWeight.bold,
color: AppColor.textPrimary,
),
),
Spacer(),
Text(
timeRemaining,
style: AppStyle.lg.copyWith(
fontWeight: FontWeight.bold,
color: AppColor.error,
),
),
],
),
SizedBox(height: 12),
SizedBox(height: 16),
// Spending requirement
GestureDetector(
onTap: () => _showSpendingInfo(draw),
child: Container(
padding: EdgeInsets.symmetric(vertical: 12, horizontal: 16),
decoration: BoxDecoration(
color: AppColor.backgroundLight,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: AppColor.borderLight, width: 1),
),
child: Row(
children: [
Expanded(
child: Text(
"Belanja min. Rp ${_formatCurrency(draw.minSpending)} untuk berpartisipasi",
// Stats row
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
children: [
Text(
"-",
style: AppStyle.h5.copyWith(
fontWeight: FontWeight.bold,
color: AppColor.textPrimary,
),
),
Text(
"Peserta",
style: AppStyle.sm.copyWith(
color: AppColor.textSecondary,
),
),
),
],
),
Column(
children: [
Text(
"${draw.hadiah}",
style: AppStyle.h5.copyWith(
fontWeight: FontWeight.bold,
color: AppColor.textPrimary,
),
),
Text(
"Hadiah",
style: AppStyle.sm.copyWith(
color: AppColor.textSecondary,
),
),
],
),
Column(
children: [
Text(
"0",
style: AppStyle.h5.copyWith(
fontWeight: FontWeight.bold,
color: AppColor.error,
),
),
Text(
"Voucher Anda",
style: AppStyle.sm.copyWith(
color: AppColor.textSecondary,
),
),
],
),
],
),
SizedBox(height: 16),
// Timer
if (draw.isActive)
Row(
children: [
Icon(
Icons.chevron_right,
color: AppColor.textSecondary,
Icons.access_time,
color: AppColor.error,
size: 16,
),
SizedBox(width: 4),
Text(
"Berakhir dalam:",
style: AppStyle.md.copyWith(
color: AppColor.textPrimary,
),
),
Spacer(),
Text(
timeRemaining,
style: AppStyle.lg.copyWith(
fontWeight: FontWeight.bold,
color: AppColor.error,
),
),
],
),
SizedBox(height: 12),
// Spending requirement
GestureDetector(
onTap: () => _showSpendingInfo(draw),
child: Container(
padding: EdgeInsets.symmetric(
vertical: 12,
horizontal: 16,
),
decoration: BoxDecoration(
color: AppColor.backgroundLight,
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: AppColor.borderLight,
width: 1,
),
),
child: Row(
children: [
Expanded(
child: Text(
"Belanja min. Rp ${_formatCurrency(draw.minSpending)} untuk berpartisipasi",
style: AppStyle.sm.copyWith(
color: AppColor.textSecondary,
),
),
),
Icon(
Icons.chevron_right,
color: AppColor.textSecondary,
size: 16,
),
],
),
),
),
),
],
],
),
),
),
],
],
),
),
);
}
@@ -0,0 +1,905 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import '../../../../../common/theme/theme.dart';
import '../../draw_page.dart';
// Prize model
class Prize {
final String id;
final String name;
final String value;
final String icon;
final int quantity;
final String description;
Prize({
required this.id,
required this.name,
required this.value,
required this.icon,
required this.quantity,
required this.description,
});
}
// Voucher model
class UserVoucher {
final String id;
final String drawId;
final String voucherNumber;
final DateTime createdDate;
final String status; // 'active', 'used', 'expired'
UserVoucher({
required this.id,
required this.drawId,
required this.voucherNumber,
required this.createdDate,
required this.status,
});
}
@RoutePage()
class DrawDetailPage extends StatefulWidget {
final DrawEvent drawEvent;
const DrawDetailPage({super.key, required this.drawEvent});
@override
State<DrawDetailPage> createState() => _DrawDetailPageState();
}
class _DrawDetailPageState extends State<DrawDetailPage>
with TickerProviderStateMixin {
late TabController _tabController;
// Sample data
final List<Prize> prizes = [
Prize(
id: "1",
name: "Emas 3 Gram",
value: "Rp 2.500.000",
icon: "👑",
quantity: 1,
description:
"Emas murni 24 karat seberat 3 gram dari toko emas terpercaya",
),
Prize(
id: "2",
name: "Emas 1 Gram",
value: "Rp 850.000",
icon: "🥇",
quantity: 1,
description:
"Emas murni 24 karat seberat 1 gram dari toko emas terpercaya",
),
];
final List<UserVoucher> userVouchers = [
UserVoucher(
id: "1",
drawId: "1",
voucherNumber: "ENK001234567",
createdDate: DateTime.now().subtract(Duration(hours: 2)),
status: 'active',
),
];
@override
void initState() {
super.initState();
_tabController = TabController(length: 4, vsync: this);
}
@override
void dispose() {
_tabController.dispose();
super.dispose();
}
String _getTimeRemaining(DateTime targetDate) {
final now = DateTime.now();
final difference = targetDate.difference(now);
if (difference.isNegative) return "Sudah berakhir";
if (difference.inDays > 0) {
return "${difference.inDays} hari ${difference.inHours % 24} jam";
} else if (difference.inHours > 0) {
return "${difference.inHours} jam ${difference.inMinutes % 60} menit";
} else if (difference.inMinutes > 0) {
return "${difference.inMinutes} menit";
} else {
return "Berakhir sekarang!";
}
}
String _formatCurrency(int amount) {
return amount.toString().replaceAllMapped(
RegExp(r'(\d{1,3})(?=(\d{3})+(?!\d))'),
(Match m) => '${m[1]}.',
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppColor.background,
body: CustomScrollView(
slivers: [
// App Bar with gradient
SliverAppBar(
expandedHeight: 200,
pinned: true,
backgroundColor: widget.drawEvent.primaryColor,
leading: IconButton(
icon: Icon(Icons.arrow_back, color: AppColor.textWhite),
onPressed: () => Navigator.of(context).pop(),
),
flexibleSpace: FlexibleSpaceBar(
background: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
widget.drawEvent.primaryColor,
widget.drawEvent.primaryColor.withOpacity(0.8),
],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
),
child: Stack(
children: [
// Background decoration
Positioned(
right: 20,
top: 60,
child: Opacity(
opacity: 0.2,
child: Column(
children: [
Row(
children: [
_buildCoin(),
SizedBox(width: 8),
_buildCoin(),
SizedBox(width: 8),
_buildCoin(),
],
),
SizedBox(height: 12),
_buildGoldBar(),
],
),
),
),
// Content
Padding(
padding: EdgeInsets.only(left: 20, right: 20, top: 100),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (widget.drawEvent.isActive)
Container(
padding: EdgeInsets.symmetric(
horizontal: 12,
vertical: 6,
),
decoration: BoxDecoration(
color: AppColor.success,
borderRadius: BorderRadius.circular(20),
),
child: Text(
"UNDIAN AKTIF",
style: AppStyle.xs.copyWith(
color: AppColor.textWhite,
fontWeight: FontWeight.bold,
),
),
),
SizedBox(height: 8),
Text(
widget.drawEvent.name,
style: AppStyle.h3.copyWith(
color: AppColor.textWhite,
fontWeight: FontWeight.bold,
),
),
Text(
widget.drawEvent.description.split('\n').first,
style: AppStyle.md.copyWith(
color: AppColor.textWhite.withOpacity(0.9),
),
),
],
),
),
],
),
),
),
),
// Tab Bar
SliverPersistentHeader(
pinned: true,
delegate: _SliverTabBarDelegate(
TabBar(
controller: _tabController,
labelColor: AppColor.primary,
unselectedLabelColor: AppColor.textSecondary,
indicatorColor: AppColor.primary,
indicatorWeight: 3,
labelStyle: AppStyle.md.copyWith(fontWeight: FontWeight.w600),
unselectedLabelStyle: AppStyle.md,
tabs: [
Tab(text: "Info"),
Tab(text: "Hadiah"),
Tab(text: "Voucher"),
Tab(text: "S&K"),
],
),
),
),
// Tab Content
SliverFillRemaining(
child: TabBarView(
controller: _tabController,
children: [
_buildInfoTab(),
_buildPrizesTab(),
_buildVouchersTab(),
_buildTermsTab(),
],
),
),
],
),
);
}
Widget _buildInfoTab() {
final timeRemaining = _getTimeRemaining(widget.drawEvent.drawDate);
return SingleChildScrollView(
padding: EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildInfoCard(
title: "Informasi Undian",
children: [
_buildInfoRow("Nama Undian", widget.drawEvent.name),
_buildInfoRow("Deskripsi", widget.drawEvent.description),
_buildInfoRow(
"Total Hadiah",
"${widget.drawEvent.hadiah} hadiah",
),
_buildInfoRow("Nilai Hadiah", widget.drawEvent.prizeValue),
_buildInfoRow(
"Status",
widget.drawEvent.isActive ? "Aktif" : "Selesai",
),
],
),
SizedBox(height: 16),
_buildInfoCard(
title: "Waktu Undian",
children: [
_buildInfoRow(
"Tanggal Pengundian",
_formatDateTime(widget.drawEvent.drawDate),
),
_buildInfoRow("Waktu Tersisa", timeRemaining),
],
),
SizedBox(height: 16),
_buildInfoCard(
title: "Statistik",
children: [
_buildInfoRow(
"Total Peserta",
"${widget.drawEvent.totalParticipants} orang",
),
_buildInfoRow(
"Voucher Anda",
"${userVouchers.where((v) => v.drawId == widget.drawEvent.id).length}",
),
_buildInfoRow(
"Minimum Belanja",
"Rp ${_formatCurrency(widget.drawEvent.minSpending)}",
),
],
),
],
),
);
}
Widget _buildPrizesTab() {
return SingleChildScrollView(
padding: EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Daftar Hadiah",
style: AppStyle.lg.copyWith(
fontWeight: FontWeight.bold,
color: AppColor.textPrimary,
),
),
SizedBox(height: 16),
...prizes.map((prize) => _buildPrizeCard(prize)).toList(),
SizedBox(height: 16),
_buildInfoCard(
title: "Ketentuan Hadiah",
children: [
_buildBulletPoint(
"Hadiah akan diumumkan setelah pengundian selesai",
),
_buildBulletPoint(
"Pemenang akan dihubungi melalui nomor telepon terdaftar",
),
_buildBulletPoint("Hadiah harus diambil dalam waktu 30 hari"),
_buildBulletPoint(
"Hadiah tidak dapat dipindahtangankan atau ditukar uang",
),
],
),
],
),
);
}
Widget _buildVouchersTab() {
final drawVouchers = userVouchers
.where((v) => v.drawId == widget.drawEvent.id)
.toList();
return SingleChildScrollView(
padding: EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Voucher Anda",
style: AppStyle.lg.copyWith(
fontWeight: FontWeight.bold,
color: AppColor.textPrimary,
),
),
SizedBox(height: 8),
Text(
"Total: ${drawVouchers.length} voucher",
style: AppStyle.md.copyWith(color: AppColor.textSecondary),
),
SizedBox(height: 16),
if (drawVouchers.isEmpty)
_buildEmptyVoucherState()
else
...drawVouchers
.map((voucher) => _buildVoucherCard(voucher))
.toList(),
SizedBox(height: 16),
_buildInfoCard(
title: "Cara Mendapat Voucher",
children: [
_buildBulletPoint(
"Lakukan pembelian minimum Rp ${_formatCurrency(widget.drawEvent.minSpending)}",
),
_buildBulletPoint("Voucher otomatis akan masuk ke akun Anda"),
_buildBulletPoint(
"Semakin banyak voucher, semakin besar peluang menang",
),
_buildBulletPoint("Voucher berlaku hingga pengundian selesai"),
],
),
],
),
);
}
Widget _buildTermsTab() {
return SingleChildScrollView(
padding: EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Syarat dan Ketentuan",
style: AppStyle.lg.copyWith(
fontWeight: FontWeight.bold,
color: AppColor.textPrimary,
),
),
SizedBox(height: 16),
_buildTermsSection(
title: "Syarat Partisipasi",
terms: [
"Pengguna harus terdaftar sebagai member Enaklo",
"Melakukan pembelian minimum Rp ${_formatCurrency(widget.drawEvent.minSpending)}",
"Pembelian harus dilakukan sebelum waktu pengundian",
"Satu transaksi pembelian = satu voucher undian",
],
),
_buildTermsSection(
title: "Ketentuan Pengundian",
terms: [
"Pengundian dilakukan secara transparan dan fair",
"Pemenang ditentukan secara acak oleh sistem",
"Keputusan pengundian bersifat final dan tidak dapat diganggu gugat",
"Pengumuman pemenang akan dilakukan maksimal 3 hari setelah pengundian",
],
),
_buildTermsSection(
title: "Ketentuan Hadiah",
terms: [
"Hadiah harus diambil dalam waktu 30 hari setelah pengumuman",
"Hadiah yang tidak diambil dalam batas waktu dianggap hangus",
"Hadiah tidak dapat ditukar dengan uang tunai",
"Pajak hadiah (jika ada) menjadi tanggung jawab pemenang",
],
),
_buildTermsSection(
title: "Ketentuan Lainnya",
terms: [
"Enaklo berhak membatalkan undian jika terjadi kecurangan",
"Peserta bertanggung jawab atas kebenaran data yang diberikan",
"Enaklo tidak bertanggung jawab atas kerugian akibat kesalahan peserta",
"Syarat dan ketentuan dapat berubah sewaktu-waktu tanpa pemberitahuan",
],
),
],
),
);
}
Widget _buildInfoCard({
required String title,
required List<Widget> children,
}) {
return Container(
width: double.infinity,
decoration: BoxDecoration(
color: AppColor.surface,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: AppColor.black.withOpacity(0.05),
blurRadius: 8,
offset: Offset(0, 2),
),
],
),
child: Padding(
padding: EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: AppStyle.lg.copyWith(
fontWeight: FontWeight.bold,
color: AppColor.textPrimary,
),
),
SizedBox(height: 12),
...children,
],
),
),
);
}
Widget _buildInfoRow(String label, String value) {
return Padding(
padding: EdgeInsets.symmetric(vertical: 4),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 120,
child: Text(
label,
style: AppStyle.sm.copyWith(color: AppColor.textSecondary),
),
),
Text(
": ",
style: AppStyle.sm.copyWith(color: AppColor.textSecondary),
),
Expanded(
child: Text(
value,
style: AppStyle.sm.copyWith(
color: AppColor.textPrimary,
fontWeight: FontWeight.w500,
),
),
),
],
),
);
}
Widget _buildPrizeCard(Prize prize) {
return Container(
margin: EdgeInsets.only(bottom: 12),
decoration: BoxDecoration(
color: AppColor.surface,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: AppColor.borderLight),
boxShadow: [
BoxShadow(
color: AppColor.black.withOpacity(0.05),
blurRadius: 8,
offset: Offset(0, 2),
),
],
),
child: Padding(
padding: EdgeInsets.all(16),
child: Row(
children: [
Container(
width: 60,
height: 60,
decoration: BoxDecoration(
color: widget.drawEvent.primaryColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(30),
),
child: Center(child: Text(prize.icon, style: AppStyle.h4)),
),
SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
prize.name,
style: AppStyle.lg.copyWith(
fontWeight: FontWeight.bold,
color: AppColor.textPrimary,
),
),
SizedBox(height: 4),
Text(
prize.value,
style: AppStyle.md.copyWith(
color: AppColor.primary,
fontWeight: FontWeight.w600,
),
),
SizedBox(height: 4),
Text(
prize.description,
style: AppStyle.sm.copyWith(color: AppColor.textSecondary),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
],
),
),
Container(
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: AppColor.primary.withOpacity(0.1),
borderRadius: BorderRadius.circular(6),
),
child: Text(
"${prize.quantity}x",
style: AppStyle.sm.copyWith(
color: AppColor.primary,
fontWeight: FontWeight.bold,
),
),
),
],
),
),
);
}
Widget _buildVoucherCard(UserVoucher voucher) {
Color statusColor;
String statusText;
switch (voucher.status) {
case 'active':
statusColor = AppColor.success;
statusText = "Aktif";
break;
case 'used':
statusColor = AppColor.textSecondary;
statusText = "Terpakai";
break;
case 'expired':
statusColor = AppColor.error;
statusText = "Kadaluarsa";
break;
default:
statusColor = AppColor.textSecondary;
statusText = "Tidak Diketahui";
}
return Container(
margin: EdgeInsets.only(bottom: 12),
decoration: BoxDecoration(
color: AppColor.surface,
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: voucher.status == 'active'
? AppColor.primary.withOpacity(0.3)
: AppColor.borderLight,
),
boxShadow: [
BoxShadow(
color: AppColor.black.withOpacity(0.05),
blurRadius: 8,
offset: Offset(0, 2),
),
],
),
child: Padding(
padding: EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Voucher Undian",
style: AppStyle.md.copyWith(
fontWeight: FontWeight.w600,
color: AppColor.textPrimary,
),
),
Container(
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: statusColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(6),
),
child: Text(
statusText,
style: AppStyle.xs.copyWith(
color: statusColor,
fontWeight: FontWeight.bold,
),
),
),
],
),
SizedBox(height: 8),
Container(
padding: EdgeInsets.all(12),
decoration: BoxDecoration(
color: AppColor.backgroundLight,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: AppColor.borderLight),
),
child: Center(
child: Text(
voucher.voucherNumber,
style: AppStyle.lg.copyWith(
fontWeight: FontWeight.bold,
color: AppColor.textPrimary,
letterSpacing: 1.2,
),
),
),
),
SizedBox(height: 8),
Text(
"Diperoleh: ${_formatDateTime(voucher.createdDate)}",
style: AppStyle.sm.copyWith(color: AppColor.textSecondary),
),
],
),
),
);
}
Widget _buildEmptyVoucherState() {
return Container(
width: double.infinity,
padding: EdgeInsets.all(32),
decoration: BoxDecoration(
color: AppColor.surface,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: AppColor.borderLight),
),
child: Column(
children: [
Icon(Icons.receipt_outlined, size: 64, color: AppColor.textLight),
SizedBox(height: 16),
Text(
"Belum Ada Voucher",
style: AppStyle.lg.copyWith(
fontWeight: FontWeight.w600,
color: AppColor.textPrimary,
),
),
SizedBox(height: 8),
Text(
"Lakukan pembelian untuk mendapatkan voucher undian",
style: AppStyle.md.copyWith(color: AppColor.textSecondary),
textAlign: TextAlign.center,
),
SizedBox(height: 16),
ElevatedButton(
onPressed: () {
// Navigate to shop or show purchase dialog
},
style: ElevatedButton.styleFrom(
backgroundColor: AppColor.primary,
foregroundColor: AppColor.textWhite,
padding: EdgeInsets.symmetric(horizontal: 24, vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
child: Text(
"Belanja Sekarang",
style: AppStyle.md.copyWith(fontWeight: FontWeight.w600),
),
),
],
),
);
}
Widget _buildTermsSection({
required String title,
required List<String> terms,
}) {
return Container(
margin: EdgeInsets.only(bottom: 24),
decoration: BoxDecoration(
color: AppColor.surface,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: AppColor.black.withOpacity(0.05),
blurRadius: 8,
offset: Offset(0, 2),
),
],
),
child: Padding(
padding: EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: AppStyle.lg.copyWith(
fontWeight: FontWeight.bold,
color: AppColor.textPrimary,
),
),
SizedBox(height: 12),
...terms.map((term) => _buildBulletPoint(term)).toList(),
],
),
),
);
}
Widget _buildBulletPoint(String text) {
return Padding(
padding: EdgeInsets.symmetric(vertical: 4),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
margin: EdgeInsets.only(top: 6),
width: 4,
height: 4,
decoration: BoxDecoration(
color: AppColor.primary,
shape: BoxShape.circle,
),
),
SizedBox(width: 12),
Expanded(
child: Text(
text,
style: AppStyle.md.copyWith(color: AppColor.textPrimary),
),
),
],
),
);
}
Widget _buildCoin() {
return Container(
width: 24,
height: 24,
decoration: BoxDecoration(
color: AppColor.warning,
shape: BoxShape.circle,
border: Border.all(color: Colors.yellow, width: 1),
),
child: Center(
child: Text(
"",
style: AppStyle.sm.copyWith(
color: Colors.orange[800],
fontWeight: FontWeight.bold,
),
),
),
);
}
Widget _buildGoldBar() {
return Container(
width: 40,
height: 20,
decoration: BoxDecoration(
color: AppColor.warning,
borderRadius: BorderRadius.circular(4),
border: Border.all(color: Colors.yellow, width: 1),
),
child: Center(
child: Text(
"GOLD",
style: AppStyle.xs.copyWith(
color: Colors.orange[800],
fontWeight: FontWeight.bold,
),
),
),
);
}
String _formatDateTime(DateTime date) {
return "${date.day}/${date.month}/${date.year} ${date.hour.toString().padLeft(2, '0')}:${date.minute.toString().padLeft(2, '0')}";
}
}
// Custom SliverTabBarDelegate
class _SliverTabBarDelegate extends SliverPersistentHeaderDelegate {
final TabBar _tabBar;
_SliverTabBarDelegate(this._tabBar);
@override
double get minExtent => _tabBar.preferredSize.height;
@override
double get maxExtent => _tabBar.preferredSize.height;
@override
Widget build(
BuildContext context,
double shrinkOffset,
bool overlapsContent,
) {
return Container(color: AppColor.surface, child: _tabBar);
}
@override
bool shouldRebuild(_SliverTabBarDelegate oldDelegate) {
return false;
}
}