import 'package:flutter/material.dart'; import '../../../../common/extension/extension.dart'; import '../../../../common/theme/theme.dart'; import '../../../../sample/reward_sample_data.dart'; import '../reward_ui.dart'; /// Kartu reward berbentuk tiket: sobekan kiri (emoji + poin) dan isi kanan. class RewardCard extends StatelessWidget { const RewardCard({super.key, required this.reward, required this.onTap}); final Reward reward; final VoidCallback onTap; static const double _stubWidth = 84; bool get _isActive => reward.status == RewardStatus.active; @override Widget build(BuildContext context) { return PhysicalShape( clipper: const _TicketClipper(notchX: _stubWidth + 0.5), color: AppColor.white, elevation: 2, shadowColor: Colors.black.withOpacity(0.25), child: Material( type: MaterialType.transparency, child: InkWell( onTap: onTap, child: IntrinsicHeight( child: Row( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ _buildStub(), const Padding( padding: EdgeInsets.symmetric(vertical: 14), child: CustomPaint( size: Size(1, double.infinity), painter: _VerticalDashPainter(), ), ), Expanded(child: _buildBody()), ], ), ), ), ), ); } Widget _buildStub() { return Container( width: _stubWidth, color: _isActive ? AppColor.primary.withOpacity(0.06) : AppColor.background, padding: const EdgeInsets.symmetric(vertical: 16), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ RewardEmoji(reward: reward, size: 34), const SizedBox(height: 8), Text( reward.pointsUsed.thousandFormat, style: AppStyle.md.copyWith( color: _isActive ? AppColor.primary : AppColor.textSecondary, fontWeight: FontWeight.w700, ), ), Text( 'poin', style: AppStyle.xs.copyWith(color: AppColor.textSecondary), ), ], ), ); } Widget _buildBody() { return Padding( padding: const EdgeInsets.fromLTRB(14, 12, 14, 12), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Expanded( child: Text( reward.categoryName.toUpperCase(), style: AppStyle.xs.copyWith( color: AppColor.textSecondary, fontWeight: FontWeight.w700, letterSpacing: 0.6, ), ), ), RewardStatusBadge(status: reward.status), ], ), const SizedBox(height: 4), Text( reward.name, maxLines: 1, overflow: TextOverflow.ellipsis, style: AppStyle.lg.copyWith( color: _isActive ? AppColor.textPrimary : AppColor.textSecondary, fontWeight: FontWeight.w700, ), ), const SizedBox(height: 2), Text( reward.description, maxLines: 1, overflow: TextOverflow.ellipsis, style: AppStyle.sm.copyWith(color: AppColor.textSecondary), ), const SizedBox(height: 10), Row( children: [ Expanded(child: _buildFootnote()), if (_isActive) ...[ const SizedBox(width: 8), Container( padding: const EdgeInsets.symmetric( horizontal: 14, vertical: 6, ), decoration: BoxDecoration( color: AppColor.primary, borderRadius: BorderRadius.circular(20), ), child: Text( 'Pakai', style: AppStyle.sm.copyWith( color: AppColor.white, fontWeight: FontWeight.w700, ), ), ), ], ], ), ], ), ); } Widget _buildFootnote() { final (IconData icon, String text, Color color) = switch (reward.status) { RewardStatus.active => ( Icons.schedule, rewardValidityLabel(reward), isRewardExpiringSoon(reward) ? AppColor.warning : AppColor.textSecondary, ), RewardStatus.used => ( Icons.check_circle_outline, reward.usedAt == null ? 'Sudah dipakai' : 'Dipakai ${formatRewardDate(reward.usedAt!)}', AppColor.textSecondary, ), RewardStatus.expired => ( Icons.event_busy_outlined, 'Berakhir ${formatRewardDate(reward.validUntil)}', AppColor.textSecondary, ), }; return Row( children: [ Icon(icon, size: 14, color: color), const SizedBox(width: 4), Expanded( child: Text( text, maxLines: 2, overflow: TextOverflow.ellipsis, style: AppStyle.xs.copyWith( color: color, fontWeight: FontWeight.w600, ), ), ), ], ); } } /// Persegi panjang membulat dengan lekuk setengah lingkaran di atas dan /// bawah pada posisi [notchX], tempat garis sobekan tiket. class _TicketClipper extends CustomClipper { const _TicketClipper({required this.notchX}); final double notchX; static const double _radius = 16; static const double _notchRadius = 8; @override Path getClip(Size size) { final card = Path() ..addRRect( RRect.fromRectAndRadius( Offset.zero & size, const Radius.circular(_radius), ), ); final notches = Path() ..addOval( Rect.fromCircle(center: Offset(notchX, 0), radius: _notchRadius), ) ..addOval( Rect.fromCircle( center: Offset(notchX, size.height), radius: _notchRadius, ), ); return Path.combine(PathOperation.difference, card, notches); } @override bool shouldReclip(covariant _TicketClipper oldClipper) => oldClipper.notchX != notchX; } class _VerticalDashPainter extends CustomPainter { const _VerticalDashPainter(); static const double _dash = 5; static const double _gap = 4; @override void paint(Canvas canvas, Size size) { final paint = Paint() ..color = AppColor.border ..strokeWidth = size.width; final x = size.width / 2; for (double y = 0; y < size.height; y += _dash + _gap) { final end = (y + _dash).clamp(0, size.height).toDouble(); canvas.drawLine(Offset(x, y), Offset(x, end), paint); } } @override bool shouldRepaint(covariant CustomPainter oldDelegate) => false; }