Files
enaklo-flutter/lib/presentation/pages/promo/promo_page.dart
T
efrilmandClaude Opus 5.5 e273785268 EnakShop: katalog toko online
- Beranda toko: pencarian, banner promo, grid kategori, produk promo,
  dan rekomendasi
- Halaman kategori dengan urutan terlaris/terbaru/termurah/termahal
- Detail produk: harga diskon, varian, info, deskripsi, produk serupa
- Data contoh 14 produk dan 3 banner; keranjang menyusul
- DetailSection dan NumberedStep jadi komponen bersama
- Tinggi banner EnakPromo dan EnakShop ikut skala font sistem

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 02:47:47 +07:00

252 lines
7.8 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(
// Ikut skala font sistem supaya teks banner tidak terpotong.
height: MediaQuery.textScalerOf(context).scale(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),
),
],
),
);
}
}