ionic-planb-logistic-app-fl.../lib/components/premium_route_card.dart
Jean-Philippe Brule 3f31a509e0 Implement premium UI/UX refinements for Apple-like polish
Add three major UI components with animations and dark mode support:

- PremiumRouteCard: Enhanced route cards with left accent bar, delivery count badge, animated hover effects (1.02x scale), and dynamic shadow elevation
- DarkModeMapComponent: Google Maps integration with dark theme styling, custom info panels, navigation buttons, and delivery status indicators
- DeliveryListItem: Animated list items with staggered entrance animations, status badges with icons, contact indicators, and hover states

Updates:
- RoutesPage: Now uses PremiumRouteCard with improved visual hierarchy
- DeliveriesPage: Integrated DarkModeMapComponent and DeliveryListItem with proper theme awareness
- Animation system: Leverages existing AppAnimations constants for 200ms fast interactions and easeOut curves

Design philosophy:
- Element separation through left accent bars (status-coded)
- Elevation and shadow for depth (0.1-0.3 opacity)
- Staggered animations for list items (50ms delays)
- Dark mode optimized for evening use (reduced brightness)
- Responsive hover states with tactile feedback

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-15 14:41:32 -05:00

253 lines
9.7 KiB
Dart

import 'package:flutter/material.dart';
import '../models/delivery_route.dart';
import '../theme/animation_system.dart';
import '../theme/color_system.dart';
class PremiumRouteCard extends StatefulWidget {
final DeliveryRoute route;
final VoidCallback onTap;
final EdgeInsets padding;
const PremiumRouteCard({
super.key,
required this.route,
required this.onTap,
this.padding = const EdgeInsets.all(16.0),
});
@override
State<PremiumRouteCard> createState() => _PremiumRouteCardState();
}
class _PremiumRouteCardState extends State<PremiumRouteCard>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _scaleAnimation;
late Animation<double> _shadowAnimation;
bool _isHovered = false;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: AppAnimations.durationFast,
vsync: this,
);
_scaleAnimation = Tween<double>(begin: 1.0, end: 1.02).animate(
CurvedAnimation(parent: _controller, curve: Curves.easeOut),
);
_shadowAnimation = Tween<double>(begin: 2.0, end: 8.0).animate(
CurvedAnimation(parent: _controller, curve: Curves.easeOut),
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
void _onHoverEnter() {
setState(() => _isHovered = true);
_controller.forward();
}
void _onHoverExit() {
setState(() => _isHovered = false);
_controller.reverse();
}
@override
Widget build(BuildContext context) {
final isDark = Theme.of(context).brightness == Brightness.dark;
final progressPercentage = (widget.route.progress * 100).toStringAsFixed(0);
final isCompleted = widget.route.progress >= 1.0;
return MouseRegion(
onEnter: (_) => _onHoverEnter(),
onExit: (_) => _onHoverExit(),
child: GestureDetector(
onTap: widget.onTap,
child: ScaleTransition(
scale: _scaleAnimation,
child: AnimatedBuilder(
animation: _shadowAnimation,
builder: (context, child) {
return Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(isDark ? 0.3 : 0.1),
blurRadius: _shadowAnimation.value,
offset: Offset(0, _shadowAnimation.value * 0.5),
),
],
),
child: child,
);
},
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
color: isDark
? const Color(0xFF14161A)
: const Color(0xFFFAFAFC),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
// Left accent bar + Header
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Accent bar
Container(
width: 4,
height: double.infinity,
decoration: BoxDecoration(
color: isCompleted
? SvrntyColors.statusCompleted
: SvrntyColors.crimsonRed,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(12),
bottomLeft: Radius.circular(12),
),
),
),
// Content
Expanded(
child: Padding(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Route name
Text(
widget.route.name,
style:
Theme.of(context).textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.w600,
letterSpacing: -0.3,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 4),
// Completion text
RichText(
text: TextSpan(
children: [
TextSpan(
text:
'${widget.route.deliveredCount}/${widget.route.deliveriesCount}',
style: Theme.of(context)
.textTheme
.bodySmall
?.copyWith(
fontWeight: FontWeight.w600,
color: isCompleted
? SvrntyColors.statusCompleted
: SvrntyColors.crimsonRed,
),
),
TextSpan(
text: ' completed',
style: Theme.of(context)
.textTheme
.bodySmall
?.copyWith(
color: isDark
? Colors.grey[400]
: Colors.grey[600],
),
),
],
),
),
],
),
),
),
// Delivery count badge
Padding(
padding: const EdgeInsets.fromLTRB(0, 16, 16, 0),
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 4,
),
decoration: BoxDecoration(
color: SvrntyColors.crimsonRed,
borderRadius: BorderRadius.circular(6),
),
child: Text(
widget.route.deliveriesCount.toString(),
style: Theme.of(context)
.textTheme
.labelSmall
?.copyWith(
color: Colors.white,
fontWeight: FontWeight.w600,
),
),
),
),
],
),
// Progress section
Padding(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Progress percentage text
Text(
'$progressPercentage% progress',
style: Theme.of(context).textTheme.labelSmall?.copyWith(
color: isDark
? Colors.grey[400]
: Colors.grey[600],
fontSize: 11,
letterSpacing: 0.3,
),
),
const SizedBox(height: 8),
// Progress bar with gradient
ClipRRect(
borderRadius: BorderRadius.circular(4),
child: Container(
height: 6,
decoration: BoxDecoration(
color: isDark
? Colors.grey[800]
: Colors.grey[200],
borderRadius: BorderRadius.circular(4),
),
child: LinearProgressIndicator(
value: widget.route.progress,
backgroundColor: Colors.transparent,
valueColor: AlwaysStoppedAnimation<Color>(
isCompleted
? SvrntyColors.statusCompleted
: SvrntyColors.crimsonRed,
),
),
),
),
],
),
),
],
),
),
),
),
),
);
}
}