Files
enaklo-flutter/lib/presentation/pages/reward/widgets/ticket_card.dart
T
efrilm 38850b281f
Build & Deploy iOS to TestFlight / build-and-deploy (push) Waiting to run
feat: integrate enakgame and voucher
2026-10-10 02:33:16 +07:00

169 lines
4.3 KiB
Dart

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<Path> {
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,
),
),
],
);
}
}