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
+4 -115
View File
@@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
import '../../../common/theme/theme.dart';
import '../../../sample/reward_sample_data.dart';
import '../../components/assets/assets.gen.dart';
import '../../components/delegate/filter_bar_delegate.dart';
import '../../router/app_router.gr.dart';
import 'reward_ui.dart';
import 'widgets/reward_card.dart';
@@ -65,8 +66,10 @@ class _RewardPageState extends State<RewardPage> {
SliverToBoxAdapter(child: _buildSummary()),
SliverPersistentHeader(
pinned: true,
delegate: _FilterHeaderDelegate(
delegate: FilterBarDelegate<_RewardFilter>(
values: _RewardFilter.values,
selected: _filter,
labelOf: (filter) => filter.label,
countOf: _countOf,
onSelected: (filter) => setState(() => _filter = filter),
),
@@ -292,117 +295,3 @@ class _RewardPageState extends State<RewardPage> {
);
}
}
class _FilterHeaderDelegate extends SliverPersistentHeaderDelegate {
_FilterHeaderDelegate({
required this.selected,
required this.countOf,
required this.onSelected,
});
final _RewardFilter selected;
final int Function(_RewardFilter) countOf;
final ValueChanged<_RewardFilter> onSelected;
static const double _height = 56;
@override
double get minExtent => _height;
@override
double get maxExtent => _height;
@override
Widget build(
BuildContext context,
double shrinkOffset,
bool overlapsContent,
) {
return Container(
color: AppColor.background,
child: ListView.separated(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
itemCount: _RewardFilter.values.length,
separatorBuilder: (_, _) => const SizedBox(width: 8),
itemBuilder: (context, index) {
final filter = _RewardFilter.values[index];
return _FilterChip(
label: filter.label,
count: countOf(filter),
isSelected: filter == selected,
onTap: () => onSelected(filter),
);
},
),
);
}
@override
bool shouldRebuild(covariant _FilterHeaderDelegate oldDelegate) =>
oldDelegate.selected != selected;
}
class _FilterChip extends StatelessWidget {
const _FilterChip({
required this.label,
required this.count,
required this.isSelected,
required this.onTap,
});
final String label;
final int count;
final bool isSelected;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
final foreground = isSelected ? AppColor.textWhite : AppColor.textPrimary;
return Material(
color: isSelected ? AppColor.primary : AppColor.white,
shape: StadiumBorder(
side: BorderSide(
color: isSelected ? AppColor.primary : AppColor.border,
),
),
child: InkWell(
customBorder: const StadiumBorder(),
onTap: onTap,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 14),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
label,
style: AppStyle.sm.copyWith(
color: foreground,
fontWeight: FontWeight.w600,
),
),
const SizedBox(width: 6),
Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 1),
decoration: BoxDecoration(
color: isSelected
? AppColor.white.withOpacity(0.2)
: AppColor.primary.withOpacity(0.08),
borderRadius: BorderRadius.circular(10),
),
child: Text(
'$count',
style: AppStyle.xs.copyWith(
color: isSelected ? AppColor.textWhite : AppColor.primary,
fontWeight: FontWeight.w700,
),
),
),
],
),
),
),
);
}
}