EnakPromo: halaman promo dan detail promo

- Banner promo unggulan, info cara dapat, filter per tipe hadiah, daftar
  promo berlangsung dan segera hadir
- Detail promo: hadiah, periode, outlet, cara dapat, S&K, CTA ke Reward
  Saya atau daftar outlet
- Data contoh mengikuti bentuk campaign backend (POINTS/COINS/REWARD)
- Filter chip Reward Saya dipindah ke komponen bersama FilterBarDelegate

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
efrilm
2026-10-01 02:17:54 +07:00
co-authored by Claude Opus 5.5
parent bc751246d8
commit c35d0eedbe
12 changed files with 1685 additions and 358 deletions
+140
View File
@@ -0,0 +1,140 @@
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import '../../../common/theme/theme.dart';
import '../../../domain/wallet/wallet.dart';
import '../../../sample/promo_sample_data.dart';
import '../../components/assets/assets.gen.dart';
import '../wallet/wallet_ui.dart';
/// Promo aktif dengan sisa hari segini dianggap segera berakhir.
const int promoEndingSoonDays = 3;
extension PromoTypeUi on PromoType {
String get label => switch (this) {
PromoType.point => 'EnakPoint',
PromoType.coin => 'EnakCoin',
PromoType.reward => 'Hadiah',
};
/// Warna aksen untuk teks dan latar tipis; cukup gelap di atas putih.
Color get color => switch (this) {
PromoType.point => AppColor.primary,
PromoType.coin => const Color(0xFFC26A00),
PromoType.reward => AppColor.secondaryDark,
};
/// Latar banner; teks putih di atasnya.
List<Color> get gradient => switch (this) {
PromoType.point => AppColor.primaryGradient,
PromoType.coin => const [Color(0xFFF59E0B), Color(0xFFC2410C)],
PromoType.reward => const [Color(0xFF43A047), Color(0xFF1B5E20)],
};
}
bool isPromoUpcoming(Promo promo) => promo.startDate.isAfter(DateTime.now());
/// Sisa hari kalender sampai [Promo.endDate]; 0 berarti berakhir hari ini.
int promoDaysLeft(Promo promo) {
final now = DateTime.now();
final today = DateTime(now.year, now.month, now.day);
final end = DateTime(
promo.endDate.year,
promo.endDate.month,
promo.endDate.day,
);
return end.difference(today).inDays;
}
bool isPromoEndingSoon(Promo promo) =>
!isPromoUpcoming(promo) && promoDaysLeft(promo) <= promoEndingSoonDays;
/// "12 Okt".
String formatPromoDate(DateTime date) =>
DateFormat('d MMM', 'id_ID').format(date);
/// "1 Okt – 25 Okt 2026".
String promoPeriodLabel(Promo promo) =>
'${formatPromoDate(promo.startDate)} – '
'${DateFormat('d MMM yyyy', 'id_ID').format(promo.endDate)}';
String promoOutletLabel(Promo promo) =>
promo.outlets.isEmpty ? 'Semua outlet' : promo.outlets.join(', ');
/// "Mulai 4 Okt" / "Berakhir 2 hari lagi" / "s/d 25 Okt".
String promoTimeLabel(Promo promo) {
if (isPromoUpcoming(promo)) {
return 'Mulai ${formatPromoDate(promo.startDate)}';
}
final daysLeft = promoDaysLeft(promo);
if (daysLeft <= 0) return 'Berakhir hari ini';
if (daysLeft <= promoEndingSoonDays) return 'Berakhir $daysLeft hari lagi';
return 's/d ${formatPromoDate(promo.endDate)}';
}
/// Ikon 3D sesuai hadiah promo.
class PromoTypeIcon extends StatelessWidget {
const PromoTypeIcon({super.key, required this.type, this.size = 32});
final PromoType type;
final double size;
@override
Widget build(BuildContext context) {
return switch (type) {
PromoType.point => WalletCurrencyIcon(
currency: WalletCurrency.point,
size: size,
),
PromoType.coin => WalletCurrencyIcon(
currency: WalletCurrency.coin,
size: size,
),
PromoType.reward => Assets.icons.enakrewardIcon.image(
width: size,
height: size,
fit: BoxFit.contain,
// Aset aslinya besar; decode seukuran tampilan saja.
cacheWidth: (size * MediaQuery.devicePixelRatioOf(context)).round(),
),
};
}
}
/// Label waktu promo: biru bila belum mulai, oranye bila segera berakhir.
class PromoTimeBadge extends StatelessWidget {
const PromoTimeBadge({super.key, required this.promo});
final Promo promo;
@override
Widget build(BuildContext context) {
final color = isPromoUpcoming(promo)
? AppColor.info
: isPromoEndingSoon(promo)
? AppColor.warning
: AppColor.textSecondary;
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(20),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.schedule, size: 12, color: color),
const SizedBox(width: 4),
Text(
promoTimeLabel(promo),
style: AppStyle.xs.copyWith(
color: color,
fontWeight: FontWeight.w700,
),
),
],
),
);
}
}