Untitled
unknown
plain_text
a month ago
22 kB
5
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