You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

449 lines
19 KiB

import 'package:flutter/material.dart';
import 'dart:io';
import 'package:quantity_input/quantity_input.dart';
import 'package:youmazgestion/Models/produit.dart';
class ProductCard extends StatefulWidget {
final Product product;
final void Function(Product, int) onAddToCart;
const ProductCard({
Key? key,
required this.product,
required this.onAddToCart,
}) : super(key: key);
@override
State<ProductCard> createState() => _ProductCardState();
}
class _ProductCardState extends State<ProductCard> with TickerProviderStateMixin {
int selectedQuantity = 1;
late AnimationController _scaleController;
late AnimationController _fadeController;
late Animation<double> _scaleAnimation;
late Animation<double> _fadeAnimation;
@override
void initState() {
super.initState();
// Animations pour les interactions
_scaleController = AnimationController(
duration: const Duration(milliseconds: 200),
vsync: this,
);
_fadeController = AnimationController(
duration: const Duration(milliseconds: 300),
vsync: this,
)..forward();
_scaleAnimation = Tween<double>(
begin: 1.0,
end: 0.95,
).animate(CurvedAnimation(
parent: _scaleController,
curve: Curves.easeInOut,
));
_fadeAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: _fadeController,
curve: Curves.easeOut,
));
}
@override
void dispose() {
_scaleController.dispose();
_fadeController.dispose();
super.dispose();
}
void _onTapDown(TapDownDetails details) {
_scaleController.forward();
}
void _onTapUp(TapUpDetails details) {
_scaleController.reverse();
}
void _onTapCancel() {
_scaleController.reverse();
}
@override
Widget build(BuildContext context) {
return FadeTransition(
opacity: _fadeAnimation,
child: AnimatedBuilder(
animation: _scaleAnimation,
builder: (context, child) {
return Transform.scale(
scale: _scaleAnimation.value,
child: Container(
margin: const EdgeInsets.all(8),
height: 280,
child: Material(
elevation: 8,
shadowColor: Colors.black.withOpacity(0.2),
borderRadius: BorderRadius.circular(20),
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
Colors.white,
Colors.grey.shade50,
],
),
),
child: ClipRRect(
borderRadius: BorderRadius.circular(20),
child: Stack(
children: [
Positioned.fill(
child: Container(
decoration: BoxDecoration(
color: Colors.grey.shade100,
borderRadius: BorderRadius.circular(20),
),
child: widget.product.image != null
? Image.file(
File(widget.product.image),
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return _buildPlaceholderImage();
},
)
: _buildPlaceholderImage(),
),
),
Positioned.fill(
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.transparent,
Colors.transparent,
Colors.black.withOpacity(0.3),
Colors.black.withOpacity(0.7),
],
stops: const [0.0, 0.4, 0.7, 1.0],
),
),
),
),
if (widget.product.isStockDefined())
Positioned(
top: 12,
right: 12,
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 4,
),
decoration: BoxDecoration(
color: Colors.green.withOpacity(0.9),
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.green.withOpacity(0.3),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(
Icons.check_circle,
color: Colors.white,
size: 12,
),
const SizedBox(width: 4),
const Text(
'En stock',
style: TextStyle(
color: Colors.white,
fontSize: 10,
fontWeight: FontWeight.bold,
),
),
],
),
),
),
Positioned(
left: 0,
right: 0,
bottom: 0,
child: Container(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 8,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
widget.product.name,
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14,
color: Colors.white,
shadows: [
Shadow(
offset: Offset(1, 1),
blurRadius: 3,
color: Colors.black54,
),
],
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 4),
Text(
'${widget.product.price.toStringAsFixed(2)} FCFA',
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 13,
color: Colors.white,
shadows: [
Shadow(
offset: Offset(1, 1),
blurRadius: 3,
color: Colors.black54,
),
],
),
),
],
),
),
const SizedBox(height: 12),
Row(
children: [
Container(
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.95),
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
_buildQuantityButton(
icon: Icons.remove,
onPressed: selectedQuantity > 1
? () {
setState(() {
selectedQuantity--;
});
}
: null,
),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 4,
),
child: Text(
'$selectedQuantity',
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14,
),
),
),
_buildQuantityButton(
icon: Icons.add,
onPressed: selectedQuantity < 100
? () {
setState(() {
selectedQuantity++;
});
}
: null,
),
],
),
),
const SizedBox(width: 8),
Expanded(
child: GestureDetector(
onTapDown: _onTapDown,
onTapUp: _onTapUp,
onTapCancel: _onTapCancel,
onTap: () {
widget.onAddToCart(widget.product, selectedQuantity);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Row(
children: [
const Icon(
Icons.shopping_cart,
color: Colors.white,
),
const SizedBox(width: 8),
Expanded(
child: Text(
'${widget.product.name} (x$selectedQuantity) ajouté au panier',
overflow: TextOverflow.ellipsis,
),
),
],
),
backgroundColor: Colors.green,
duration: const Duration(seconds: 1),
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
);
},
child: Container(
padding: const EdgeInsets.symmetric(
vertical: 10,
horizontal: 12,
),
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [
Color.fromARGB(255, 4, 54, 95),
Color.fromARGB(255, 6, 80, 140),
],
),
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: const Color.fromARGB(255, 4, 54, 95).withOpacity(0.3),
blurRadius: 6,
offset: const Offset(0, 3),
),
],
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons.add_shopping_cart,
color: Colors.white,
size: 16,
),
const SizedBox(width: 6),
const Flexible(
child: Text(
'Ajouter',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 12,
),
overflow: TextOverflow.ellipsis,
),
),
],
),
),
),
),
],
),
],
),
),
),
],
),
),
),
),
),
);
},
),
);
}
Widget _buildPlaceholderImage() {
return Container(
width: double.infinity,
height: double.infinity,
decoration: BoxDecoration(
color: Colors.grey.shade200,
borderRadius: BorderRadius.circular(20),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.image_outlined,
size: 40,
color: Colors.grey.shade400,
),
const SizedBox(height: 8),
Text(
'Image non disponible',
style: TextStyle(
color: Colors.grey.shade500,
fontSize: 12,
),
),
],
),
);
}
Widget _buildQuantityButton({
required IconData icon,
required VoidCallback? onPressed,
}) {
return Material(
color: Colors.transparent,
child: InkWell(
onTap: onPressed,
borderRadius: BorderRadius.circular(15),
child: Container(
padding: const EdgeInsets.all(6),
child: Icon(
icon,
size: 16,
color: onPressed != null ? const Color.fromARGB(255, 4, 54, 95) : Colors.grey,
),
),
),
);
}
}