From afa528abd69f91a304e6404887859e5787a343ef Mon Sep 17 00:00:00 2001 From: Suherdy Yacob Date: Sun, 14 Jun 2026 11:19:28 +0700 Subject: [PATCH] style: redesign subscription list items as beautiful tear-off tickets with notched edges and dashed separator lines, removing all similarities to loyalty cards --- lib/widgets/subscription_list_widget.dart | 285 ++++++++++++++-------- 1 file changed, 177 insertions(+), 108 deletions(-) diff --git a/lib/widgets/subscription_list_widget.dart b/lib/widgets/subscription_list_widget.dart index 1777cfa..d45f283 100644 --- a/lib/widgets/subscription_list_widget.dart +++ b/lib/widgets/subscription_list_widget.dart @@ -1,8 +1,7 @@ import 'package:flutter/material.dart'; -import '../theme/app_theme.dart'; /// "My Subscriptions" list displayed on the home tab below the loyalty card. -/// Renders each subscription as a beautiful standalone card visually distinct +/// Renders each subscription as a beautiful standalone ticket card visually distinct /// from the points-based loyalty card. class SubscriptionListWidget extends StatelessWidget { final List subscriptions; @@ -13,6 +12,9 @@ class SubscriptionListWidget extends StatelessWidget { Widget build(BuildContext context) { if (subscriptions.isEmpty) return const SizedBox.shrink(); + final theme = Theme.of(context); + final colorScheme = theme.colorScheme; + return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -20,8 +22,8 @@ class SubscriptionListWidget extends StatelessWidget { padding: const EdgeInsets.fromLTRB(16, 24, 16, 8), child: Text( 'MY SUBSCRIPTIONS', - style: Theme.of(context).textTheme.labelLarge?.copyWith( - color: AppTheme.onSurfaceVariant, + style: theme.textTheme.labelLarge?.copyWith( + color: colorScheme.onSurfaceVariant, letterSpacing: 1.2, fontSize: 11, fontWeight: FontWeight.bold, @@ -85,129 +87,196 @@ class _SubscriptionCard extends StatelessWidget { final endDate = _formatDate(sub['subscription_end_date']); final active = _isActive(); - return Container( - margin: const EdgeInsets.fromLTRB(16, 8, 16, 8), - padding: const EdgeInsets.all(20), - decoration: BoxDecoration( + return Padding( + padding: const EdgeInsets.fromLTRB(16, 8, 16, 8), + child: PhysicalShape( + clipper: TicketClipper(), color: colorScheme.surfaceContainerLowest, - border: Border.all( - color: active ? colorScheme.secondary.withValues(alpha: 0.4) : colorScheme.outline.withValues(alpha: 0.4), - width: 1.5, - ), - borderRadius: BorderRadius.circular(16), - boxShadow: [ - BoxShadow( - color: Colors.black.withValues(alpha: 0.04), - blurRadius: 10, - offset: const Offset(0, 4), - ), - ], - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + elevation: 3, + shadowColor: Colors.black.withValues(alpha: 0.15), + child: Container( + padding: const EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ + // Top Section: Title & Status Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Icon( - Icons.card_membership_rounded, - size: 18, - color: active ? colorScheme.secondary : colorScheme.outline, + Row( + children: [ + Icon( + Icons.local_activity_rounded, // Distinct ticket pass icon + size: 18, + color: active ? colorScheme.primary : colorScheme.outline, + ), + const SizedBox(width: 8), + Text( + 'SUBSCRIPTION PASS', // Distinct title + style: theme.textTheme.labelLarge?.copyWith( + color: active ? colorScheme.primary : colorScheme.outline, + fontWeight: FontWeight.bold, + fontSize: 10, + letterSpacing: 1.2, + ), + ), + ], ), - const SizedBox(width: 8), - Text( - 'SUBSCRIPTION CARD', - style: theme.textTheme.labelLarge?.copyWith( - color: active ? colorScheme.secondary : colorScheme.outline, - fontWeight: FontWeight.bold, - fontSize: 10, - letterSpacing: 1.0, - ), + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: active + ? const Color(0xFF1B5E20).withValues(alpha: 0.10) + : const Color(0xFFB02500).withValues(alpha: 0.08), + borderRadius: BorderRadius.circular(20), + ), + child: Text( + active ? 'ACTIVE' : 'EXPIRED', + style: TextStyle( + fontSize: 9, + fontWeight: FontWeight.bold, + letterSpacing: 0.8, + color: active + ? const Color(0xFF2E7D32) + : const Color(0xFFB02500), + ), + ), ), ], ), - Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), - decoration: BoxDecoration( - color: active - ? const Color(0xFF1B5E20).withValues(alpha: 0.10) - : const Color(0xFFB02500).withValues(alpha: 0.08), - borderRadius: BorderRadius.circular(20), // Pill status badge - ), - child: Text( - active ? 'ACTIVE' : 'EXPIRED', - style: TextStyle( - fontSize: 9, - fontWeight: FontWeight.bold, - letterSpacing: 0.8, - color: active - ? const Color(0xFF2E7D32) - : const Color(0xFFB02500), - ), + const SizedBox(height: 18), + // Dashed Separator + CustomPaint( + size: const Size(double.infinity, 1), + painter: DashedLinePainter( + color: colorScheme.outline.withValues(alpha: 0.25), ), ), - ], - ), - const SizedBox(height: 14), - Text( - programName, - style: theme.textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.bold, - color: colorScheme.onSurface, - fontFamily: 'serif', - ), - ), - const SizedBox(height: 12), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Card Number', - style: theme.textTheme.bodySmall?.copyWith( - color: colorScheme.onSurfaceVariant, - fontSize: 10, - ), - ), - const SizedBox(height: 2), - Text( - code.isNotEmpty ? code : 'N/A', - style: theme.textTheme.bodyMedium?.copyWith( - fontFamily: 'monospace', - fontWeight: FontWeight.bold, - color: colorScheme.onSurface, - ), - ), - ], + const SizedBox(height: 18), + // Bottom Section: Details + Text( + programName, + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.bold, + color: colorScheme.onSurface, + fontFamily: 'serif', + ), ), - Column( - crossAxisAlignment: CrossAxisAlignment.end, + const SizedBox(height: 12), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text( - 'Validity Period', - style: theme.textTheme.bodySmall?.copyWith( - color: colorScheme.onSurfaceVariant, - fontSize: 10, - ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Pass Code', // Distinct field label + style: theme.textTheme.bodySmall?.copyWith( + color: colorScheme.onSurfaceVariant, + fontSize: 10, + ), + ), + const SizedBox(height: 2), + Text( + code.isNotEmpty ? code : 'N/A', + style: theme.textTheme.bodyMedium?.copyWith( + fontFamily: 'monospace', + fontWeight: FontWeight.bold, + color: colorScheme.onSurface, + ), + ), + ], ), - const SizedBox(height: 2), - Text( - '$startDate - $endDate', - style: theme.textTheme.bodyMedium?.copyWith( - color: colorScheme.onSurface, - fontWeight: FontWeight.w600, - ), + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + 'Validity Period', + style: theme.textTheme.bodySmall?.copyWith( + color: colorScheme.onSurfaceVariant, + fontSize: 10, + ), + ), + const SizedBox(height: 2), + Text( + '$startDate - $endDate', + style: theme.textTheme.bodyMedium?.copyWith( + color: colorScheme.onSurface, + fontWeight: FontWeight.w600, + ), + ), + ], ), ], ), ], ), - ], + ), ), ); } } + +/// Custom Clipper for a ticket-like appearance with side notches. +class TicketClipper extends CustomClipper { + @override + Path getClip(Size size) { + final path = Path(); + path.lineTo(0, 0); + path.lineTo(size.width, 0); + + // Right side notch at height 49 (approx. center of vertical separator space) + const double notchY = 49.0; + const double notchRadius = 8.0; + path.lineTo(size.width, notchY - notchRadius); + path.arcToPoint( + Offset(size.width, notchY + notchRadius), + radius: const Radius.circular(notchRadius), + clockwise: false, + ); + path.lineTo(size.width, size.height); + path.lineTo(0, size.height); + + // Left side notch + path.lineTo(0, notchY + notchRadius); + path.arcToPoint( + const Offset(0, notchY - notchRadius), + radius: const Radius.circular(notchRadius), + clockwise: false, + ); + path.close(); + return path; + } + + @override + bool shouldReclip(CustomClipper oldClipper) => false; +} + +/// Painter to draw a clean dashed divider line. +class DashedLinePainter extends CustomPainter { + final Color color; + + DashedLinePainter({required this.color}); + + @override + void paint(Canvas canvas, Size size) { + double dashWidth = 5.0; + double dashSpace = 4.0; + double startX = 0.0; + final paint = Paint() + ..color = color + ..strokeWidth = 1.0; + + while (startX < size.width) { + canvas.drawLine( + Offset(startX, 0), + Offset(startX + dashWidth, 0), + paint, + ); + startX += dashWidth + dashSpace; + } + } + + @override + bool shouldRepaint(CustomPainter oldDelegate) => false; +}