baseproject
This commit is contained in:
355
base_project/lib/shared/widgets/cards/dashboard_card.dart
Normal file
355
base_project/lib/shared/widgets/cards/dashboard_card.dart
Normal file
@@ -0,0 +1,355 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../core/constants/ui_constants.dart';
|
||||
|
||||
enum DashboardCardType {
|
||||
primary,
|
||||
secondary,
|
||||
success,
|
||||
warning,
|
||||
info,
|
||||
danger,
|
||||
}
|
||||
|
||||
class DashboardCard extends StatefulWidget {
|
||||
final String title;
|
||||
final String? subtitle;
|
||||
final String? value;
|
||||
final num? numericValue;
|
||||
final String? valueSuffix;
|
||||
final Duration? animationDuration;
|
||||
final IconData? icon;
|
||||
final DashboardCardType type;
|
||||
final VoidCallback? onTap;
|
||||
final Widget? trailing;
|
||||
final bool isLoading;
|
||||
final Color? customColor;
|
||||
final double? elevation;
|
||||
final BorderRadius? borderRadius;
|
||||
|
||||
const DashboardCard({
|
||||
super.key,
|
||||
required this.title,
|
||||
this.subtitle,
|
||||
this.value,
|
||||
this.numericValue,
|
||||
this.valueSuffix,
|
||||
this.animationDuration,
|
||||
this.icon,
|
||||
this.type = DashboardCardType.primary,
|
||||
this.onTap,
|
||||
this.trailing,
|
||||
this.isLoading = false,
|
||||
this.customColor,
|
||||
this.elevation,
|
||||
this.borderRadius,
|
||||
});
|
||||
|
||||
@override
|
||||
State<DashboardCard> createState() => _DashboardCardState();
|
||||
}
|
||||
|
||||
class _DashboardCardState extends State<DashboardCard>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late AnimationController _animationController;
|
||||
late Animation<double> _scaleAnimation;
|
||||
late Animation<double> _elevationAnimation;
|
||||
bool _isPressed = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_animationController = AnimationController(
|
||||
duration: UIConstants.durationFast,
|
||||
vsync: this,
|
||||
);
|
||||
|
||||
_scaleAnimation = Tween<double>(
|
||||
begin: 1.0,
|
||||
end: 0.95,
|
||||
).animate(CurvedAnimation(
|
||||
parent: _animationController,
|
||||
curve: UIConstants.curveFast,
|
||||
));
|
||||
|
||||
_elevationAnimation = Tween<double>(
|
||||
begin: 1.0,
|
||||
end: 0.0,
|
||||
).animate(CurvedAnimation(
|
||||
parent: _animationController,
|
||||
curve: UIConstants.curveFast,
|
||||
));
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_animationController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _onTapDown(TapDownDetails details) {
|
||||
if (widget.onTap != null && !widget.isLoading) {
|
||||
setState(() {
|
||||
_isPressed = true;
|
||||
});
|
||||
_animationController.forward();
|
||||
}
|
||||
}
|
||||
|
||||
void _onTapUp(TapUpDetails details) {
|
||||
if (widget.onTap != null && !widget.isLoading) {
|
||||
setState(() {
|
||||
_isPressed = false;
|
||||
});
|
||||
_animationController.reverse();
|
||||
}
|
||||
}
|
||||
|
||||
void _onTapCancel() {
|
||||
if (widget.onTap != null && !widget.isLoading) {
|
||||
setState(() {
|
||||
_isPressed = false;
|
||||
});
|
||||
_animationController.reverse();
|
||||
}
|
||||
}
|
||||
|
||||
Color _getCardColor(ThemeData theme) {
|
||||
if (widget.customColor != null) return widget.customColor!;
|
||||
|
||||
switch (widget.type) {
|
||||
case DashboardCardType.primary:
|
||||
return theme.colorScheme.primary;
|
||||
case DashboardCardType.secondary:
|
||||
return theme.colorScheme.secondary;
|
||||
case DashboardCardType.success:
|
||||
return theme.colorScheme.tertiary;
|
||||
case DashboardCardType.warning:
|
||||
return const Color(0xFFF59E0B);
|
||||
case DashboardCardType.info:
|
||||
return theme.colorScheme.primary;
|
||||
case DashboardCardType.danger:
|
||||
return theme.colorScheme.error;
|
||||
}
|
||||
}
|
||||
|
||||
Color _getIconColor(ThemeData theme) {
|
||||
if (widget.customColor != null) return Colors.white;
|
||||
|
||||
switch (widget.type) {
|
||||
case DashboardCardType.primary:
|
||||
case DashboardCardType.secondary:
|
||||
case DashboardCardType.success:
|
||||
case DashboardCardType.warning:
|
||||
case DashboardCardType.info:
|
||||
case DashboardCardType.danger:
|
||||
return Colors.white;
|
||||
}
|
||||
}
|
||||
|
||||
double _getElevation() {
|
||||
if (widget.elevation != null) return widget.elevation!;
|
||||
return UIConstants.elevation4;
|
||||
}
|
||||
|
||||
BorderRadius _getBorderRadius() {
|
||||
if (widget.borderRadius != null) return widget.borderRadius!;
|
||||
return BorderRadius.circular(UIConstants.radius16);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final colorScheme = theme.colorScheme;
|
||||
final cardColor = _getCardColor(theme);
|
||||
final iconColor = _getIconColor(theme);
|
||||
|
||||
return AnimatedBuilder(
|
||||
animation: _animationController,
|
||||
builder: (context, child) {
|
||||
return Transform.scale(
|
||||
scale: _scaleAnimation.value,
|
||||
child: GestureDetector(
|
||||
onTapDown: _onTapDown,
|
||||
onTapUp: _onTapUp,
|
||||
onTapCancel: _onTapCancel,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: _getBorderRadius(),
|
||||
color: cardColor,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: cardColor.withOpacity(0.3),
|
||||
blurRadius: 8 * _elevationAnimation.value,
|
||||
offset: Offset(0, 4 * _elevationAnimation.value),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: widget.onTap,
|
||||
borderRadius: _getBorderRadius(),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(UIConstants.spacing16),
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final isTight = constraints.maxHeight < 150;
|
||||
final isVeryTight = constraints.maxHeight < 130;
|
||||
final spacing16 = isTight
|
||||
? UIConstants.spacing8
|
||||
: UIConstants.spacing16;
|
||||
final spacing12 = isTight
|
||||
? UIConstants.spacing6
|
||||
: UIConstants.spacing12;
|
||||
final iconSize = isTight
|
||||
? UIConstants.iconSizeMedium
|
||||
: UIConstants.iconSizeLarge;
|
||||
final titleStyle =
|
||||
theme.textTheme.titleMedium?.copyWith(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: isVeryTight ? 12 : (isTight ? 14 : null),
|
||||
);
|
||||
final subtitleStyle =
|
||||
theme.textTheme.bodyMedium?.copyWith(
|
||||
color: Colors.white.withOpacity(0.8),
|
||||
fontSize: isVeryTight ? 10 : (isTight ? 12 : null),
|
||||
);
|
||||
final valueStyle =
|
||||
theme.textTheme.headlineSmall?.copyWith(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w700,
|
||||
fontSize: isVeryTight ? 16 : (isTight ? 18 : null),
|
||||
);
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Header Row
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
// Icon
|
||||
if (widget.icon != null)
|
||||
Container(
|
||||
padding: EdgeInsets.all(spacing12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(
|
||||
UIConstants.radius12),
|
||||
),
|
||||
child: Icon(
|
||||
widget.icon,
|
||||
color: iconColor,
|
||||
size: iconSize,
|
||||
),
|
||||
),
|
||||
|
||||
// Trailing Widget
|
||||
if (widget.trailing != null) widget.trailing!,
|
||||
],
|
||||
),
|
||||
|
||||
SizedBox(height: spacing16),
|
||||
|
||||
// Title
|
||||
Text(
|
||||
widget.title,
|
||||
style: titleStyle,
|
||||
maxLines: isTight ? 1 : 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
|
||||
// Subtitle
|
||||
if (widget.subtitle != null && !isVeryTight) ...[
|
||||
SizedBox(height: spacing12),
|
||||
Text(
|
||||
widget.subtitle!,
|
||||
style: subtitleStyle,
|
||||
maxLines: isTight ? 1 : 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
|
||||
// Value (animated if numeric)
|
||||
SizedBox(height: spacing16),
|
||||
if (widget.numericValue != null)
|
||||
TweenAnimationBuilder<double>(
|
||||
tween: Tween<double>(
|
||||
begin: 0,
|
||||
end: widget.numericValue!.toDouble()),
|
||||
duration: widget.animationDuration ??
|
||||
UIConstants.durationNormal,
|
||||
curve: UIConstants.curveNormal,
|
||||
builder: (context, value, child) {
|
||||
final text =
|
||||
_formatNumber(value, widget.valueSuffix);
|
||||
final valueText = Text(
|
||||
text,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: valueStyle,
|
||||
);
|
||||
return isTight
|
||||
? FittedBox(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: valueText)
|
||||
: valueText;
|
||||
},
|
||||
)
|
||||
else if (widget.value != null)
|
||||
Builder(builder: (context) {
|
||||
final valueText = Text(
|
||||
widget.value!,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: valueStyle,
|
||||
);
|
||||
return isTight
|
||||
? FittedBox(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: valueText)
|
||||
: valueText;
|
||||
}),
|
||||
|
||||
// Loading Indicator
|
||||
if (widget.isLoading) ...[
|
||||
SizedBox(height: spacing16),
|
||||
const LinearProgressIndicator(
|
||||
backgroundColor: Colors.white24,
|
||||
valueColor:
|
||||
AlwaysStoppedAnimation<Color>(Colors.white),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
String _formatNumber(double value, String? suffix) {
|
||||
String formatted;
|
||||
if (value >= 1000000) {
|
||||
formatted = "${(value / 1000000).toStringAsFixed(1)}M";
|
||||
} else if (value >= 1000) {
|
||||
formatted = "${(value / 1000).toStringAsFixed(1)}K";
|
||||
} else {
|
||||
if (value == value.roundToDouble()) {
|
||||
formatted = value.toInt().toString();
|
||||
} else {
|
||||
formatted = value.toStringAsFixed(1);
|
||||
}
|
||||
}
|
||||
if (suffix != null && suffix.isNotEmpty) {
|
||||
return "$formatted$suffix";
|
||||
}
|
||||
return formatted;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user