688 lines
23 KiB
Dart
688 lines
23 KiB
Dart
import 'package:flutter/material.dart';
|
||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||
import 'package:carousel_slider/carousel_slider.dart';
|
||
import 'package:get/get.dart';
|
||
import 'package:wordpress/providers/api_const.dart';
|
||
import 'package:wordpress/providers/api_controller.dart';
|
||
import 'package:wordpress/utils/app_assests.dart';
|
||
import 'package:wordpress/view/all_product_screen.dart';
|
||
import 'package:wordpress/view/details_screen.dart';
|
||
import 'package:wordpress/viewmodel/cart_screen.dart';
|
||
import 'package:wordpress/viewmodel/product_home_page_model.dart';
|
||
|
||
class HomeScreen extends ConsumerStatefulWidget {
|
||
const HomeScreen({super.key});
|
||
|
||
@override
|
||
ConsumerState<HomeScreen> createState() => _HomeScreenState();
|
||
}
|
||
|
||
class _HomeScreenState extends ConsumerState<HomeScreen>
|
||
with TickerProviderStateMixin {
|
||
late AnimationController _fadeController;
|
||
late AnimationController _slideController;
|
||
late AnimationController _scaleController;
|
||
|
||
late Animation<double> _fadeAnimation;
|
||
late Animation<Offset> _slideAnimation;
|
||
late Animation<double> _scaleAnimation;
|
||
|
||
@override
|
||
void initState() {
|
||
super.initState();
|
||
|
||
// Initialize animation controllers
|
||
_fadeController = AnimationController(
|
||
duration: const Duration(milliseconds: 800),
|
||
vsync: this,
|
||
);
|
||
|
||
_slideController = AnimationController(
|
||
duration: const Duration(milliseconds: 1000),
|
||
vsync: this,
|
||
);
|
||
|
||
_scaleController = AnimationController(
|
||
duration: const Duration(milliseconds: 600),
|
||
vsync: this,
|
||
);
|
||
|
||
// Initialize animations
|
||
_fadeAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
|
||
CurvedAnimation(parent: _fadeController, curve: Curves.easeInOut),
|
||
);
|
||
|
||
_slideAnimation =
|
||
Tween<Offset>(begin: const Offset(0, 0.3), end: Offset.zero).animate(
|
||
CurvedAnimation(parent: _slideController, curve: Curves.easeOutBack),
|
||
);
|
||
|
||
_scaleAnimation = Tween<double>(begin: 0.8, end: 1.0).animate(
|
||
CurvedAnimation(parent: _scaleController, curve: Curves.elasticOut),
|
||
);
|
||
|
||
// Start animations
|
||
_startAnimations();
|
||
}
|
||
|
||
void _startAnimations() async {
|
||
await Future.delayed(const Duration(milliseconds: 100));
|
||
_fadeController.forward();
|
||
|
||
await Future.delayed(const Duration(milliseconds: 200));
|
||
_slideController.forward();
|
||
|
||
await Future.delayed(const Duration(milliseconds: 300));
|
||
_scaleController.forward();
|
||
}
|
||
|
||
@override
|
||
void dispose() {
|
||
_fadeController.dispose();
|
||
_slideController.dispose();
|
||
_scaleController.dispose();
|
||
super.dispose();
|
||
}
|
||
|
||
String getFullImageUrl(String url) {
|
||
if (url.startsWith("http")) {
|
||
return url;
|
||
}
|
||
return "${ConstsApi.baseUrl}$url";
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final homeAsync = ref.watch(productHomePageProvider);
|
||
|
||
return Scaffold(
|
||
backgroundColor: Colors.grey[50],
|
||
body: SafeArea(
|
||
child: homeAsync.when(
|
||
data: (data) {
|
||
return SingleChildScrollView(
|
||
physics: const BouncingScrollPhysics(),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
ScaleTransition(
|
||
scale: _scaleAnimation,
|
||
child: _buildHeaderSection(),
|
||
),
|
||
// 🎯 Section 1: Animated Banner Carousel
|
||
FadeTransition(
|
||
opacity: _fadeAnimation,
|
||
child: _buildBannerSection(data.banners),
|
||
),
|
||
|
||
const SizedBox(height: 24),
|
||
|
||
// 🎯 Section 2: Animated Categories
|
||
SlideTransition(
|
||
position: _slideAnimation,
|
||
child: _buildCategoriesSection(data.categories),
|
||
),
|
||
|
||
const SizedBox(height: 24),
|
||
|
||
// 🎯 Section 3: Animated Latest Products
|
||
ScaleTransition(
|
||
scale: _scaleAnimation,
|
||
child: _buildLatestProductsSection(data.latest),
|
||
),
|
||
|
||
const SizedBox(height: 20),
|
||
],
|
||
),
|
||
);
|
||
},
|
||
loading: () => _buildLoadingState(),
|
||
error: (err, stack) => _buildErrorState(err),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
// 🎨 Banner Section with Animation
|
||
Widget _buildBannerSection(List<BannerModel> banners) {
|
||
if (banners.isEmpty) {
|
||
return const SizedBox.shrink();
|
||
}
|
||
|
||
return Container(
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
const SizedBox(height: 25),
|
||
CarouselSlider(
|
||
options: CarouselOptions(
|
||
height: 160,
|
||
autoPlay: true,
|
||
enlargeCenterPage: true,
|
||
viewportFraction: 0.9,
|
||
autoPlayInterval: const Duration(seconds: 4),
|
||
autoPlayAnimationDuration: const Duration(milliseconds: 800),
|
||
autoPlayCurve: Curves.fastOutSlowIn,
|
||
),
|
||
items: banners.map((banner) {
|
||
final url = getFullImageUrl(banner.image);
|
||
return ClipRRect(
|
||
borderRadius: BorderRadius.circular(16),
|
||
child: Container(
|
||
color: Colors.grey[200], // fallback bg
|
||
child: Image.network(
|
||
url,
|
||
width: double.infinity,
|
||
height: 160,
|
||
fit: BoxFit
|
||
.fill, // fills both width & height // change to BoxFit.contain if you don’t want cropping
|
||
errorBuilder: (context, error, stackTrace) => Image.network(
|
||
'https://project2022.amrithaa.com/ecomdemo/wp-content/uploads/2025/08/product1.jpg',
|
||
width: double.infinity,
|
||
height: 160,
|
||
fit: BoxFit.cover,
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}).toList(),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildHeaderSection() {
|
||
return Padding(
|
||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||
child: Row(
|
||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||
children: [
|
||
Image.asset(AppAssets.logo, height: 60, width: 100),
|
||
GestureDetector(
|
||
onTap: () {
|
||
Get.to(CartScreen());
|
||
},
|
||
child: Container(
|
||
width: 40,
|
||
height: 40,
|
||
decoration: BoxDecoration(
|
||
color: Colors.white,
|
||
|
||
shape: BoxShape.circle,
|
||
boxShadow: [
|
||
BoxShadow(
|
||
color: Colors.black.withOpacity(0.1),
|
||
blurRadius: 6,
|
||
offset: const Offset(0, 3),
|
||
),
|
||
],
|
||
),
|
||
child: Icon(Icons.shopping_cart, color: Colors.blue),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
// 🎨 Categories Section with Animation
|
||
Widget _buildCategoriesSection(List<CategoryModel> categories) {
|
||
if (categories.isEmpty) {
|
||
return const SizedBox.shrink();
|
||
}
|
||
|
||
return Container(
|
||
margin: const EdgeInsets.symmetric(horizontal: 16),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
const Text(
|
||
"Shop by Category",
|
||
style: TextStyle(
|
||
fontSize: 20,
|
||
fontWeight: FontWeight.bold,
|
||
color: Colors.black87,
|
||
),
|
||
),
|
||
const SizedBox(height: 12),
|
||
SizedBox(
|
||
height: 130,
|
||
child: ListView.builder(
|
||
scrollDirection: Axis.horizontal,
|
||
physics: const BouncingScrollPhysics(),
|
||
itemCount: categories.length,
|
||
itemBuilder: (context, index) {
|
||
final category = categories[index];
|
||
return TweenAnimationBuilder<double>(
|
||
duration: Duration(milliseconds: 400 + (index * 100)),
|
||
tween: Tween(begin: 0.0, end: 1.0),
|
||
builder: (context, value, child) {
|
||
return Transform.scale(
|
||
scale: value,
|
||
child: Container(
|
||
width: 100,
|
||
margin: const EdgeInsets.only(right: 12),
|
||
child: Column(
|
||
children: [
|
||
Container(
|
||
height: 80,
|
||
width: 80,
|
||
decoration: BoxDecoration(
|
||
color: Colors
|
||
.primaries[index % Colors.primaries.length]
|
||
.withOpacity(0.1),
|
||
borderRadius: BorderRadius.circular(20),
|
||
border: Border.all(
|
||
color: Colors
|
||
.primaries[index %
|
||
Colors.primaries.length]
|
||
.withOpacity(0.3),
|
||
width: 2,
|
||
),
|
||
),
|
||
child: category.image != null
|
||
? ClipRRect(
|
||
borderRadius: BorderRadius.circular(18),
|
||
child: Image.network(
|
||
getFullImageUrl(category.image!),
|
||
fit: BoxFit.cover,
|
||
errorBuilder:
|
||
(
|
||
context,
|
||
error,
|
||
stackTrace,
|
||
) => Icon(
|
||
Icons.category,
|
||
color:
|
||
Colors.primaries[index %
|
||
Colors.primaries.length],
|
||
size: 30,
|
||
),
|
||
),
|
||
)
|
||
: Icon(
|
||
Icons.category,
|
||
color:
|
||
Colors.primaries[index %
|
||
Colors.primaries.length],
|
||
size: 30,
|
||
),
|
||
),
|
||
const SizedBox(height: 8),
|
||
Text(
|
||
category.name,
|
||
textAlign: TextAlign.center,
|
||
maxLines: 2,
|
||
overflow: TextOverflow.ellipsis,
|
||
style: const TextStyle(
|
||
fontSize: 12,
|
||
fontWeight: FontWeight.w500,
|
||
color: Colors.black87,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
},
|
||
);
|
||
},
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
// 🎨 Latest Products Section with Animation
|
||
Widget _buildLatestProductsSection(List<ProductModel> products) {
|
||
if (products.isEmpty) {
|
||
return const SizedBox.shrink();
|
||
}
|
||
|
||
return Container(
|
||
margin: const EdgeInsets.symmetric(horizontal: 16),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Row(
|
||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||
children: [
|
||
const Text(
|
||
"Latest Products",
|
||
style: TextStyle(
|
||
fontSize: 20,
|
||
fontWeight: FontWeight.bold,
|
||
color: Colors.black87,
|
||
),
|
||
),
|
||
TextButton(
|
||
onPressed: () {
|
||
Navigator.push(
|
||
context,
|
||
MaterialPageRoute(builder: (context) => AllProductScreen()),
|
||
);
|
||
},
|
||
child: const Text(
|
||
"View All",
|
||
style: TextStyle(
|
||
color: Colors.blue,
|
||
fontWeight: FontWeight.w600,
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
const SizedBox(height: 12),
|
||
GridView.builder(
|
||
shrinkWrap: true,
|
||
physics: const NeverScrollableScrollPhysics(),
|
||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||
crossAxisCount: 2,
|
||
childAspectRatio: 0.75,
|
||
crossAxisSpacing: 12,
|
||
mainAxisSpacing: 12,
|
||
),
|
||
itemCount: products.length > 6 ? 6 : products.length,
|
||
itemBuilder: (context, index) {
|
||
final product = products[index];
|
||
return TweenAnimationBuilder<double>(
|
||
duration: Duration(milliseconds: 500 + (index * 150)),
|
||
tween: Tween(begin: 0.0, end: 1.0),
|
||
builder: (context, value, child) {
|
||
return Transform.translate(
|
||
offset: Offset(0, 30 * (1 - value)),
|
||
child: Opacity(
|
||
opacity: value,
|
||
child: _buildProductCard(product),
|
||
),
|
||
);
|
||
},
|
||
);
|
||
},
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
// 🎨 Product Card with Hover Effect
|
||
Widget _buildProductCard(ProductModel product) {
|
||
return GestureDetector(
|
||
onTap: () {
|
||
Navigator.push(
|
||
context,
|
||
MaterialPageRoute(
|
||
builder: (context) => BigBasketDetailsScreen(productId: product.id),
|
||
),
|
||
);
|
||
},
|
||
child: AnimatedContainer(
|
||
duration: const Duration(milliseconds: 200),
|
||
decoration: BoxDecoration(
|
||
color: Colors.white,
|
||
borderRadius: BorderRadius.circular(16),
|
||
boxShadow: [
|
||
BoxShadow(
|
||
color: Colors.black.withOpacity(0.08),
|
||
blurRadius: 8,
|
||
offset: const Offset(0, 4),
|
||
),
|
||
],
|
||
),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
// Product Image
|
||
Expanded(
|
||
flex: 3,
|
||
child: Container(
|
||
decoration: const BoxDecoration(
|
||
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
|
||
),
|
||
child: ClipRRect(
|
||
borderRadius: const BorderRadius.vertical(
|
||
top: Radius.circular(16),
|
||
),
|
||
child: Stack(
|
||
children: [
|
||
Image.network(
|
||
getFullImageUrl(product.image),
|
||
width: double.infinity,
|
||
height: double.infinity,
|
||
fit: BoxFit.cover,
|
||
errorBuilder: (context, error, stackTrace) => Container(
|
||
color: Colors.grey[100],
|
||
child: const Icon(
|
||
Icons.image_not_supported,
|
||
color: Colors.grey,
|
||
size: 40,
|
||
),
|
||
),
|
||
),
|
||
// Gradient overlay
|
||
Positioned(
|
||
bottom: 0,
|
||
left: 0,
|
||
right: 0,
|
||
child: Container(
|
||
height: 30,
|
||
decoration: BoxDecoration(
|
||
gradient: LinearGradient(
|
||
begin: Alignment.topCenter,
|
||
end: Alignment.bottomCenter,
|
||
colors: [
|
||
Colors.transparent,
|
||
Colors.black.withOpacity(0.1),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
),
|
||
|
||
// Product Details
|
||
Expanded(
|
||
flex: 2,
|
||
child: Padding(
|
||
padding: const EdgeInsets.all(12),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||
children: [
|
||
Text(
|
||
product.name,
|
||
maxLines: 2,
|
||
overflow: TextOverflow.ellipsis,
|
||
style: const TextStyle(
|
||
fontSize: 14,
|
||
fontWeight: FontWeight.w600,
|
||
color: Colors.black87,
|
||
height: 1.2,
|
||
),
|
||
),
|
||
const SizedBox(height: 4),
|
||
Row(
|
||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||
children: [
|
||
Text(
|
||
"₹${product.price}",
|
||
style: const TextStyle(
|
||
fontSize: 16,
|
||
fontWeight: FontWeight.bold,
|
||
color: Colors.green,
|
||
),
|
||
),
|
||
GestureDetector(
|
||
child: Container(
|
||
padding: const EdgeInsets.all(6),
|
||
decoration: BoxDecoration(
|
||
color: Colors.blue,
|
||
borderRadius: BorderRadius.circular(8),
|
||
),
|
||
child: const Icon(
|
||
Icons.add_shopping_cart,
|
||
color: Colors.white,
|
||
size: 16,
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
// 🎨 Loading State with Animation
|
||
Widget _buildLoadingState() {
|
||
return Center(
|
||
child: Column(
|
||
mainAxisAlignment: MainAxisAlignment.center,
|
||
children: [
|
||
TweenAnimationBuilder<double>(
|
||
duration: const Duration(seconds: 1),
|
||
tween: Tween(begin: 0.0, end: 1.0),
|
||
builder: (context, value, child) {
|
||
return Transform.rotate(
|
||
angle: value * 2 * 3.14159,
|
||
child: Container(
|
||
width: 60,
|
||
height: 60,
|
||
decoration: BoxDecoration(
|
||
borderRadius: BorderRadius.circular(30),
|
||
gradient: const LinearGradient(
|
||
colors: [Colors.blue, Colors.purple],
|
||
),
|
||
),
|
||
child: const Icon(
|
||
Icons.shopping_bag,
|
||
color: Colors.white,
|
||
size: 30,
|
||
),
|
||
),
|
||
);
|
||
},
|
||
),
|
||
const SizedBox(height: 16),
|
||
const Text(
|
||
"Loading amazing products...",
|
||
style: TextStyle(
|
||
fontSize: 16,
|
||
color: Colors.grey,
|
||
fontWeight: FontWeight.w500,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
// 🎨 Error State
|
||
Widget _buildErrorState(dynamic error) {
|
||
return Center(
|
||
child: Column(
|
||
mainAxisAlignment: MainAxisAlignment.center,
|
||
children: [
|
||
TweenAnimationBuilder<double>(
|
||
duration: const Duration(milliseconds: 800),
|
||
tween: Tween(begin: 0.0, end: 1.0),
|
||
builder: (context, value, child) {
|
||
return Transform.scale(
|
||
scale: value,
|
||
child: Container(
|
||
width: 80,
|
||
height: 80,
|
||
decoration: BoxDecoration(
|
||
color: Colors.red.withOpacity(0.1),
|
||
borderRadius: BorderRadius.circular(40),
|
||
),
|
||
child: const Icon(
|
||
Icons.error_outline,
|
||
color: Colors.red,
|
||
size: 40,
|
||
),
|
||
),
|
||
);
|
||
},
|
||
),
|
||
const SizedBox(height: 16),
|
||
const Text(
|
||
"Oops! Something went wrong",
|
||
style: TextStyle(
|
||
fontSize: 18,
|
||
fontWeight: FontWeight.w600,
|
||
color: Colors.black87,
|
||
),
|
||
),
|
||
const SizedBox(height: 8),
|
||
Text(
|
||
"Error: $error",
|
||
textAlign: TextAlign.center,
|
||
style: TextStyle(fontSize: 14, color: Colors.grey[600]),
|
||
),
|
||
const SizedBox(height: 16),
|
||
ElevatedButton(
|
||
onPressed: () {
|
||
// Retry logic
|
||
ref.invalidate(productHomePageProvider);
|
||
},
|
||
style: ElevatedButton.styleFrom(
|
||
backgroundColor: Colors.blue,
|
||
foregroundColor: Colors.white,
|
||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
||
shape: RoundedRectangleBorder(
|
||
borderRadius: BorderRadius.circular(12),
|
||
),
|
||
),
|
||
child: const Text(
|
||
"Try Again",
|
||
style: TextStyle(fontWeight: FontWeight.w600),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
// 🎨 Additional Animation Extensions for Enhanced UI
|
||
extension AnimatedWidgetExtensions on Widget {
|
||
Widget fadeInUp({
|
||
Duration duration = const Duration(milliseconds: 600),
|
||
Duration delay = Duration.zero,
|
||
}) {
|
||
return TweenAnimationBuilder<double>(
|
||
duration: duration,
|
||
tween: Tween(begin: 0.0, end: 1.0),
|
||
builder: (context, value, child) {
|
||
return Transform.translate(
|
||
offset: Offset(0, 20 * (1 - value)),
|
||
child: Opacity(opacity: value, child: this),
|
||
);
|
||
},
|
||
);
|
||
}
|
||
|
||
Widget bounceIn({Duration duration = const Duration(milliseconds: 800)}) {
|
||
return TweenAnimationBuilder<double>(
|
||
duration: duration,
|
||
tween: Tween(begin: 0.0, end: 1.0),
|
||
curve: Curves.elasticOut,
|
||
builder: (context, value, child) {
|
||
return Transform.scale(scale: value, child: this);
|
||
},
|
||
);
|
||
}
|
||
}
|