Untitled
unknown
plain_text
7 months ago
22 kB
8
Indexable
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:hsl/models/product.dart';
import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../../../providers/product_provider.dart';
import 'home_screen.dart';
import 'product_detail_page.dart';
class SearchPage extends StatefulWidget {
const SearchPage({super.key});
@override
_SearchPageState createState() => _SearchPageState();
}
class _SearchPageState extends State<SearchPage> {
TextEditingController _searchController = TextEditingController();
String _searchQuery = '';
List<String> selectedCategories = [];
double minPrice = 0.0;
double maxPrice = 1000.0;
List<Product> filteredProducts = [];
List<Product> searchProduct = [];
ScrollController _scrollController = ScrollController();
int _currentPage = 1;
bool _isLoading = false;
bool _hasMoreProducts = true;
@override
void initState() {
super.initState();
_searchController.addListener(_onSearchChanged);
_scrollController.addListener(_onScroll);
_fetchFilteredProducts();
}
@override
void dispose() {
_searchController.dispose();
_scrollController.dispose();
super.dispose();
}
void _onSearchChanged() {
setState(() {
_searchQuery = _searchController.text.toLowerCase();
_fetchFilteredProducts(isNewSearch: true);
});
}
void _onScroll() {
if (_scrollController.position.pixels ==
_scrollController.position.maxScrollExtent &&
!_isLoading &&
_hasMoreProducts) {
_fetchFilteredProducts();
}
}
Future<void> _fetchFilteredProducts({bool isNewSearch = false}) async {
if (_isLoading) return;
setState(() {
_isLoading = true;
if (isNewSearch) {
_currentPage = 1;
filteredProducts.clear(); // Ensure old products are removed
searchProduct.clear();
_hasMoreProducts = true; // Reset load more state
}
});
final productProvider = Provider.of<ProductProvider>(context, listen: false);
final products = await productProvider.fetchProducts(
id_categories: selectedCategories.isNotEmpty ? selectedCategories : null,
min_price: minPrice > 0 ? minPrice : null,
max_price: maxPrice > 0 ? maxPrice : null,
name: _searchQuery.isNotEmpty ? _searchQuery : null,
page: _currentPage,
limit: 10,
);
setState(() {
if (products != null && products.isNotEmpty) {
if (isNewSearch) {
filteredProducts = products; // Replace products for a new search
} else {
filteredProducts.addAll(products); // Append for pagination
}
searchProduct = List.from(filteredProducts); // Update displayed products
_currentPage++;
} else {
_hasMoreProducts = false;
}
_isLoading = false;
});
}
Future<void> _saveFilterValues() async {
final prefs = await SharedPreferences.getInstance();
await prefs.setString('selectedCategories', jsonEncode(selectedCategories));
await prefs.setDouble('minPrice', minPrice);
await prefs.setDouble('maxPrice', maxPrice);
}
void _showFilterModal() {
Set<String> tempCategories = Set.from(selectedCategories);
double tempMinPrice = minPrice;
double tempMaxPrice = maxPrice;
final tempMinPriceController =
TextEditingController(text: minPrice.toString());
final tempMaxPriceController =
TextEditingController(text: maxPrice.toString());
showModalBottomSheet(
context: context,
isScrollControlled: true,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
builder: (BuildContext context) {
final productProvider =
Provider.of<ProductProvider>(context, listen: false);
final categories = productProvider.categories;
print("Categories: $categories");
// Fetch categories from the provider
return StatefulBuilder(
builder: (BuildContext context, StateSetter modalSetState) {
return Padding(
padding: EdgeInsets.only(
left: 16.0,
right: 16.0,
top: 16.0,
bottom: MediaQuery.of(context).viewInsets.bottom,
),
child: Container(
height: 460,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text('Filter',
style: TextStyle(
fontSize: 21, fontWeight: FontWeight.w600)),
TextButton(
onPressed: () {
modalSetState(() {
tempCategories.clear();
tempMinPrice = 0.0;
tempMaxPrice = 1000.0;
tempMinPriceController.text = '0.0';
tempMaxPriceController.text = '1000.0';
});
},
child: const Text('Clear',
style: TextStyle(
color: Color(0xFFA1A1A1),
fontSize: 15,
fontWeight: FontWeight.w600)),
),
],
),
const SizedBox(height: 24),
Expanded(
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Category',
style: TextStyle(
color: Colors.black,
fontSize: 15,
fontWeight: FontWeight.w500,
),),
const SizedBox(height: 12),
Wrap(
spacing: 10,
children: categories
.where((category) =>
category.toLowerCase() != 'home' &&
category.toLowerCase() != 'offers')
.map((category) {
bool isSelected = tempCategories.contains(category);
return FilterChip(
label: Text(category, style: TextStyle(
color: isSelected?Colors.white: Color(0xFF595959),
fontSize: 14,
fontWeight: FontWeight.w500,
),),
selected: isSelected,
checkmarkColor: Colors.white,
selectedColor: const Color(0xFF08A657),
onSelected: (bool value) {
modalSetState(() {
if (value) {
tempCategories.add(category);
} else {
tempCategories.remove(category);
}
});
},
);
}).toList(),
),
const SizedBox(height: 24),
const Text('Price (Between min and max)',
style: TextStyle(
color: Colors.black,
fontSize: 15,
fontWeight: FontWeight.w500,
),),
const SizedBox(height: 18),
Row(
children: [
Expanded(
child: TextField(
controller: tempMinPriceController,
decoration: const InputDecoration(
labelText: 'Min MAD',
hintText: '0.00',
labelStyle:
TextStyle(color: Colors.black),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Color(0xFF08A657),
width: 2.0),
),
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Colors.black, width: 1.0),
),
),
keyboardType:
const TextInputType.numberWithOptions(
decimal: true),
),
),
const SizedBox(width: 10),
Expanded(
child: TextField(
controller: tempMaxPriceController,
decoration: const InputDecoration(
labelText: 'Max MAD',
hintText: '1000.00',
labelStyle:
TextStyle(color: Colors.black),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Color(0xFF08A657),
width: 2.0),
),
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Colors.black, width: 1.0),
),
),
keyboardType:
const TextInputType.numberWithOptions(
decimal: true),
),
),
],
),
],
),
),
),
Padding(
padding: const EdgeInsets.only(bottom: 18.0),
child: ElevatedButton(
onPressed: () {
double newMinPrice =
double.tryParse(tempMinPriceController.text) ??
minPrice;
double newMaxPrice =
double.tryParse(tempMaxPriceController.text) ??
maxPrice;
if (newMinPrice > newMaxPrice) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text(
'Min price cannot be greater than max price')),
);
return;
}
setState(() {
selectedCategories = tempCategories.toList();
minPrice = newMinPrice;
maxPrice = newMaxPrice;
});
_saveFilterValues();
Navigator.pop(context);
// Immediately fetch products with the new filter values
_fetchFilteredProducts(isNewSearch: true);
},
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF08A657),
minimumSize: const Size(double.infinity, 50),
),
child: const Text('Apply filter',
style: TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.w600)),
),
),
],
),
));
},
);
},
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
resizeToAvoidBottomInset: true,
body: Stack(
children: [
// Background green app bar
ClipPath(
clipper: CustomAppBarShapeClipper(),
child: Container(
color: const Color(0xFF08A657),
height:
140, // This should be large enough for your custom clipper
),
),
Padding(
padding: const EdgeInsets.fromLTRB(16, 50, 16, 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Search bar with back button and filter icon
Container(
height: 50,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(25),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
spreadRadius: 1,
blurRadius: 5,
),
],
),
child: Row(
children: [
IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () => Navigator.pop(context),
),
Expanded(
child: TextField(
controller: _searchController,
decoration: const InputDecoration(
hintText: 'Search...',
border: InputBorder.none,
),
),
),
const Text(
'Filter', // Desired text
style: TextStyle(
fontSize: 16, // Font size
color: Color(0xFF08A657), // Text color
),
),
IconButton(
icon: const Icon(Icons.filter_list,
color: Color(0xFF08A657)),
onPressed: _showFilterModal,
),
],
),
),
const SizedBox(height: 20),
Text(
'Products found (${searchProduct.length})',
style: TextStyle(
color: Color(0xFFB3B3B3),
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 10),
// Product list
Expanded(
child: _isLoading && searchProduct.isEmpty
? Center(child: CircularProgressIndicator())
: ListView.builder(
padding: const EdgeInsets.only(top: 10.0),
itemCount: searchProduct.length + (_hasMoreProducts ? 1 : 0),
controller: _scrollController,
itemBuilder: (context, index) {
if (index < searchProduct.length) {
final product = searchProduct[index];
return Padding(
padding: const EdgeInsets.only(bottom: 16.0),
child: _buildProductCard(product, context),
);
} else {
// Show a loading indicator when fetching more data
return Center(
child: Padding(
padding: const EdgeInsets.all(10.0),
child: CircularProgressIndicator(),
),
);
}
},
),
),
],
),
),
],
),
);
}
Widget _buildProductCard(dynamic product, BuildContext context) {
return GestureDetector(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ProductDetailPage(product: product),
),
);
},
child: Card(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
side: const BorderSide(
color: Color(0xffF3F3F3),
width: 1,
),
),
color: Colors.white,
elevation: 0,
child: Row(
children: [
ClipRRect(
borderRadius:
const BorderRadius.horizontal(left: Radius.circular(16)),
child: Image.network(
product.imgUrl,
fit: BoxFit.cover,
width: 100,
height: 100,
errorBuilder: (context, error, stackTrace) =>
const Icon(Icons.error),
),
),
Expanded(
child: Padding(
padding: const EdgeInsets.all(8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
product.isNew == true ? 'New' : '',
style: TextStyle(
color: product.isNew == true
? Color(0xFFE2AB00)
: Colors.transparent,
fontSize: 11,
fontWeight: FontWeight.w600,
height: 1.82,
),
),
Text(
product.name['en'],
style: const TextStyle(
fontSize: 18, fontWeight: FontWeight.bold),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 12),
Text(
product.description['en'] ?? '',
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: const TextStyle(fontSize: 14),
),
const SizedBox(height: 20),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'${product.publicPrice.toStringAsFixed(2)} MAD',
style: const TextStyle(
fontWeight: FontWeight.bold,
color: Color(0xFF08A657)),
),
Container(
padding:
const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: product.offer == null
? Colors.transparent
: Colors.red.shade50,
borderRadius: BorderRadius.circular(30),
),
child: Text(
product.offer == null ? '' : '- ${product.offer} %',
style: TextStyle(
color: Color(0xFFE10C0C),
fontSize: 10,
fontWeight: FontWeight.w500,
height: 2,
),
),
),
],
),
],
),
),
),
],
),
),
);
}
}Editor is loading...
Leave a Comment