- 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>
251 lines
7.7 KiB
Dart
251 lines
7.7 KiB
Dart
import 'package:auto_route/auto_route.dart';
|
|
import 'package:flutter/material.dart';
|
|
|
|
import '../../../common/theme/theme.dart';
|
|
import '../../../sample/promo_sample_data.dart';
|
|
import '../../components/delegate/filter_bar_delegate.dart';
|
|
import '../../router/app_router.gr.dart';
|
|
import 'promo_ui.dart';
|
|
import 'widgets/promo_banner.dart';
|
|
import 'widgets/promo_card.dart';
|
|
|
|
enum _PromoFilter {
|
|
all('Semua'),
|
|
point('EnakPoint'),
|
|
coin('EnakCoin'),
|
|
reward('Hadiah');
|
|
|
|
const _PromoFilter(this.label);
|
|
|
|
final String label;
|
|
|
|
bool matches(Promo promo) => switch (this) {
|
|
_PromoFilter.all => true,
|
|
_PromoFilter.point => promo.type == PromoType.point,
|
|
_PromoFilter.coin => promo.type == PromoType.coin,
|
|
_PromoFilter.reward => promo.type == PromoType.reward,
|
|
};
|
|
}
|
|
|
|
@RoutePage()
|
|
class PromoPage extends StatefulWidget {
|
|
const PromoPage({super.key});
|
|
|
|
@override
|
|
State<PromoPage> createState() => _PromoPageState();
|
|
}
|
|
|
|
class _PromoPageState extends State<PromoPage> {
|
|
final PageController _bannerController = PageController(
|
|
viewportFraction: 0.9,
|
|
);
|
|
int _bannerIndex = 0;
|
|
_PromoFilter _filter = _PromoFilter.all;
|
|
|
|
/// Promo yang belum berakhir, paling cepat berakhir di atas.
|
|
final List<Promo> _promos =
|
|
promoSamples.where((p) => p.endDate.isAfter(DateTime.now())).toList()
|
|
..sort((a, b) => a.endDate.compareTo(b.endDate));
|
|
|
|
List<Promo> get _featured =>
|
|
_promos.where((p) => p.isFeatured && !isPromoUpcoming(p)).toList();
|
|
|
|
int _countOf(_PromoFilter filter) => _promos.where(filter.matches).length;
|
|
|
|
void _open(Promo promo) =>
|
|
context.router.push(PromoDetailRoute(promo: promo));
|
|
|
|
@override
|
|
void dispose() {
|
|
_bannerController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final visible = _promos.where(_filter.matches).toList();
|
|
final ongoing = visible.where((p) => !isPromoUpcoming(p)).toList();
|
|
final upcoming = visible.where(isPromoUpcoming).toList();
|
|
final featured = _featured;
|
|
|
|
return Scaffold(
|
|
backgroundColor: AppColor.background,
|
|
appBar: AppBar(title: const Text('EnakPromo')),
|
|
body: CustomScrollView(
|
|
slivers: [
|
|
if (featured.isNotEmpty)
|
|
SliverToBoxAdapter(child: _buildBanners(featured)),
|
|
SliverToBoxAdapter(child: _buildHowItWorks()),
|
|
SliverPersistentHeader(
|
|
pinned: true,
|
|
delegate: FilterBarDelegate<_PromoFilter>(
|
|
values: _PromoFilter.values,
|
|
selected: _filter,
|
|
labelOf: (filter) => filter.label,
|
|
countOf: _countOf,
|
|
onSelected: (filter) => setState(() => _filter = filter),
|
|
),
|
|
),
|
|
if (visible.isEmpty)
|
|
SliverFillRemaining(hasScrollBody: false, child: _buildEmpty())
|
|
else ...[
|
|
if (ongoing.isNotEmpty)
|
|
..._buildSection('Sedang berlangsung', ongoing),
|
|
if (upcoming.isNotEmpty) ..._buildSection('Segera hadir', upcoming),
|
|
const SliverToBoxAdapter(child: SizedBox(height: 24)),
|
|
],
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildBanners(List<Promo> featured) {
|
|
return Column(
|
|
children: [
|
|
const SizedBox(height: 16),
|
|
SizedBox(
|
|
height: 168,
|
|
child: PageView.builder(
|
|
controller: _bannerController,
|
|
itemCount: featured.length,
|
|
onPageChanged: (index) => setState(() => _bannerIndex = index),
|
|
itemBuilder: (context, index) {
|
|
final promo = featured[index];
|
|
return Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 6),
|
|
child: PromoBanner(promo: promo, onTap: () => _open(promo)),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
const SizedBox(height: 10),
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: List.generate(featured.length, (index) {
|
|
final isActive = index == _bannerIndex;
|
|
return AnimatedContainer(
|
|
duration: const Duration(milliseconds: 200),
|
|
margin: const EdgeInsets.symmetric(horizontal: 3),
|
|
width: isActive ? 18 : 6,
|
|
height: 6,
|
|
decoration: BoxDecoration(
|
|
color: isActive ? AppColor.primary : AppColor.border,
|
|
borderRadius: BorderRadius.circular(3),
|
|
),
|
|
);
|
|
}),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildHowItWorks() {
|
|
return Container(
|
|
margin: const EdgeInsets.fromLTRB(16, 16, 16, 4),
|
|
padding: const EdgeInsets.all(12),
|
|
decoration: BoxDecoration(
|
|
color: AppColor.white,
|
|
borderRadius: BorderRadius.circular(AppValue.borderRadius),
|
|
border: Border.all(color: AppColor.borderLight),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
Container(
|
|
width: 36,
|
|
height: 36,
|
|
decoration: BoxDecoration(
|
|
color: AppColor.primary.withOpacity(0.08),
|
|
shape: BoxShape.circle,
|
|
),
|
|
child: const Icon(
|
|
Icons.phone_iphone_rounded,
|
|
size: 18,
|
|
color: AppColor.primary,
|
|
),
|
|
),
|
|
const SizedBox(width: 12),
|
|
Expanded(
|
|
child: Text.rich(
|
|
TextSpan(
|
|
children: [
|
|
TextSpan(
|
|
text: 'Tanpa klaim. ',
|
|
style: AppStyle.sm.copyWith(fontWeight: FontWeight.w700),
|
|
),
|
|
const TextSpan(
|
|
text:
|
|
'Sebutkan nomor HP akunmu ke kasir saat bayar, bonus '
|
|
'promo masuk otomatis.',
|
|
),
|
|
],
|
|
),
|
|
style: AppStyle.sm.copyWith(color: AppColor.textSecondary),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
List<Widget> _buildSection(String title, List<Promo> promos) {
|
|
return [
|
|
SliverPadding(
|
|
padding: const EdgeInsets.fromLTRB(16, 4, 16, 10),
|
|
sliver: SliverToBoxAdapter(
|
|
child: Text(
|
|
title,
|
|
style: AppStyle.md.copyWith(fontWeight: FontWeight.w700),
|
|
),
|
|
),
|
|
),
|
|
SliverPadding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
|
sliver: SliverList.separated(
|
|
itemCount: promos.length,
|
|
separatorBuilder: (_, _) => const SizedBox(height: 12),
|
|
itemBuilder: (context, index) {
|
|
final promo = promos[index];
|
|
return PromoCard(promo: promo, onTap: () => _open(promo));
|
|
},
|
|
),
|
|
),
|
|
const SliverToBoxAdapter(child: SizedBox(height: 16)),
|
|
];
|
|
}
|
|
|
|
Widget _buildEmpty() {
|
|
return Padding(
|
|
padding: const EdgeInsets.fromLTRB(32, 32, 32, 64),
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Container(
|
|
width: 96,
|
|
height: 96,
|
|
alignment: Alignment.center,
|
|
decoration: BoxDecoration(
|
|
color: AppColor.primary.withOpacity(0.06),
|
|
shape: BoxShape.circle,
|
|
),
|
|
child: const Text('🏷️', style: TextStyle(fontSize: 44)),
|
|
),
|
|
const SizedBox(height: 16),
|
|
Text(
|
|
_filter == _PromoFilter.all
|
|
? 'Belum ada promo'
|
|
: 'Belum ada promo ${_filter.label}',
|
|
textAlign: TextAlign.center,
|
|
style: AppStyle.lg.copyWith(fontWeight: FontWeight.w700),
|
|
),
|
|
const SizedBox(height: 6),
|
|
Text(
|
|
'Promo baru akan muncul di sini.',
|
|
textAlign: TextAlign.center,
|
|
style: AppStyle.sm.copyWith(color: AppColor.textSecondary),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|