import 'package:flutter/material.dart'; import '../../../../common/theme/theme.dart'; /// Kartu berbentuk tiket: sobekan kiri ([stub]) dan isi kanan ([body]), /// dipisah garis putus-putus. class TicketCard extends StatelessWidget { const TicketCard({ super.key, required this.stub, required this.body, required this.onTap, this.active = true, }); final Widget stub; final Widget body; final VoidCallback onTap; /// Latar sobekan berwarna untuk voucher yang bisa ditukar / dipakai. final bool active; static const double stubWidth = 84; @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: [ Container( width: stubWidth, color: active ? AppColor.primary.withOpacity(0.06) : AppColor.background, padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 16, ), child: stub, ), const Padding( padding: EdgeInsets.symmetric(vertical: 14), child: CustomPaint( size: Size(1, double.infinity), painter: _VerticalDashPainter(), ), ), Expanded(child: body), ], ), ), ), ), ); } } /// 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; } /// Isi sobekan: gambar voucher dan EnakPoint yang ditukar. class TicketStub extends StatelessWidget { const TicketStub({ super.key, required this.image, required this.pointCost, this.active = true, }); final Widget image; final String pointCost; final bool active; @override Widget build(BuildContext context) { return Column( mainAxisAlignment: MainAxisAlignment.center, children: [ image, const SizedBox(height: 8), FittedBox( fit: BoxFit.scaleDown, child: Text( pointCost, style: AppStyle.md.copyWith( color: active ? AppColor.primary : AppColor.textSecondary, fontWeight: FontWeight.w700, ), ), ), Text( 'EnakPoint', style: AppStyle.xs.copyWith( fontSize: 10, color: AppColor.textSecondary, ), ), ], ); } }