import 'dart:async'; import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:get/get.dart'; import 'package:http/http.dart' as http; import 'package:nearledaily/constants/color_constants.dart'; import '../../constants/font_constants.dart'; import '../../controllers/tenant_controller /tenant_list.dart'; import '../../widgets/text_widget.dart'; import '../product/tenant_products.dart'; // ─── Search result modules ───────────────────────────────────────────────────── class _SearchResult { final String tenantName; final String productName; final String subCatName; const _SearchResult({ required this.tenantName, required this.productName, required this.subCatName, }); factory _SearchResult.fromJson(Map json) => _SearchResult( tenantName: json['tenantname'] ?? '', productName: json['productname'] ?? '', subCatName: json['subcatname'] ?? '', ); } // ─── Screen ────────────────────────────────────────────────────────────────── class SearchScreen extends StatefulWidget { const SearchScreen({Key? key}) : super(key: key); @override State createState() => _SearchScreenState(); } class _SearchScreenState extends State with SingleTickerProviderStateMixin { final TextEditingController _searchController = TextEditingController(); final FocusNode _focusNode = FocusNode(); final TenantController tenantController = Get.find(); late AnimationController _animationController; late Animation _fadeAnimation; late Animation _slideAnimation; Timer? _debounce; bool _isSearching = false; List<_SearchResult> _searchResults = []; String _lastQuery = ''; static const String _searchBaseUrl = 'https://fiesta.nearle.app/live/api/v1/mob/tenants/searchbykeyword'; @override void initState() { super.initState(); _animationController = AnimationController( vsync: this, duration: const Duration(milliseconds: 400), ); _fadeAnimation = CurvedAnimation( parent: _animationController, curve: Curves.easeOut, ); _slideAnimation = Tween( begin: const Offset(0, 0.05), end: Offset.zero, ).animate(CurvedAnimation( parent: _animationController, curve: Curves.easeOut, )); _animationController.forward(); _focusNode.addListener(() => setState(() {})); _searchController.addListener(_onSearchChanged); } @override void dispose() { _debounce?.cancel(); _animationController.dispose(); _searchController.removeListener(_onSearchChanged); _searchController.dispose(); _focusNode.dispose(); super.dispose(); } void _onSearchChanged() { final query = _searchController.text.trim(); setState(() {}); if (query == _lastQuery) return; _lastQuery = query; _debounce?.cancel(); if (query.isEmpty) { setState(() { _searchResults = []; _isSearching = false; }); return; } _debounce = Timer(const Duration(milliseconds: 400), () { _fetchSearchResults(query); }); } Future _fetchSearchResults(String keyword) async { if (!mounted) return; setState(() => _isSearching = true); try { final uri = Uri.parse( '$_searchBaseUrl?keyword=${Uri.encodeComponent(keyword)}'); final response = await http.get(uri).timeout(const Duration(seconds: 10)); if (!mounted) return; if (response.statusCode == 200) { final data = jsonDecode(response.body) as Map; if (data['status'] == true && data['details'] is List) { final results = (data['details'] as List) .map((e) => _SearchResult.fromJson(e as Map)) .toList(); setState(() { _searchResults = results; _isSearching = false; }); return; } } setState(() { _searchResults = []; _isSearching = false; }); } catch (_) { if (!mounted) return; setState(() { _searchResults = []; _isSearching = false; }); } } List get _filteredTenants { final query = _searchController.text.trim(); if (query.isEmpty) return tenantController.searchtenants; final matchedNames = _searchResults.map((r) => r.tenantName.toLowerCase()).toSet(); return tenantController.searchtenants .where( (t) => matchedNames.contains((t.tenantname ?? '').toLowerCase())) .toList(); } Widget _imagePlaceholder() { return Container( decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [Colors.grey.shade100, Colors.grey.shade400], ), ), child: Center( child: Icon(Icons.store_outlined, size: 48, color: Colors.grey.shade400), ), ); } void _navigateTo(dynamic item) { HapticFeedback.lightImpact(); Get.to( () => ProductsScreen( tenantId: item.tenantid!, locationId: item.locationid!, categoryId: item.categoryid!, tenantName: item.tenantname!, locationname: item.locationname!, tenantLocation: item.suburb!, tenantImage: item.tenantimage!, tenantloc: item.locationid!, subCategoryName: "", ), transition: Transition.cupertino, duration: const Duration(milliseconds: 300), ); } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: const Color(0xFFF6F6F6), body: Column( children: [ // ── Search bar ──────────────────────────────────────────────── SafeArea( bottom: false, child: FadeTransition( opacity: _fadeAnimation, child: Padding( padding: const EdgeInsets.fromLTRB(16, 16, 16, 12), child: Hero( tag: 'search_bar', child: Material( color: Colors.transparent, child: Container( height: 52, padding: const EdgeInsets.symmetric(horizontal: 12), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(16), border: Border.all( color: _focusNode.hasFocus ? ColorConstants.primaryColor : Colors.grey.shade300, width: _focusNode.hasFocus ? 2 : 1, ), boxShadow: [ BoxShadow( color: _focusNode.hasFocus ? ColorConstants.primaryColor.withOpacity(0.15) : Colors.black.withOpacity(0.06), blurRadius: _focusNode.hasFocus ? 12 : 8, offset: const Offset(0, 4), ), ], ), child: Row( children: [ IconButton( icon: const Icon(Icons.arrow_back), onPressed: () => Navigator.pop(context), ), const SizedBox(width: 6), Expanded( child: TextField( controller: _searchController, focusNode: _focusNode, autofocus: true, style: const TextStyle( fontSize: 16, fontWeight: FontWeight.w500, ), decoration: InputDecoration( hintText: 'Search stores or products...', hintStyle: TextStyle( color: Colors.grey.shade400, fontWeight: FontWeight.normal, ), border: InputBorder.none, ), ), ), if (_isSearching) SizedBox( width: 20, height: 20, child: CircularProgressIndicator( strokeWidth: 2, color: ColorConstants.primaryColor, ), ) else if (_searchController.text.isNotEmpty) IconButton( icon: const Icon(Icons.clear, size: 20), onPressed: () { _searchController.clear(); setState(() { _searchResults = []; _lastQuery = ''; }); }, ), ], ), ), ), ), ), ), ), // ── List ────────────────────────────────────────────────────── Expanded( child: Obx(() { if (tenantController.isLoading.value) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ CircularProgressIndicator( color: ColorConstants.primaryColor), const SizedBox(height: 16), Text( 'Loading stores...', style: TextStyle( color: Colors.grey.shade600, fontSize: 14), ), ], ), ); } final displayTenants = _filteredTenants; final hasQuery = _searchController.text.trim().isNotEmpty; if (displayTenants.isEmpty) { return FadeTransition( opacity: _fadeAnimation, child: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.search_off_rounded, size: 80, color: Colors.grey.shade300), const SizedBox(height: 16), Text( hasQuery ? 'No matching stores found' : 'No stores found', style: TextStyle( fontSize: 18, fontWeight: FontWeight.w600, color: Colors.grey.shade600, ), ), const SizedBox(height: 8), Text( 'Try searching with different keywords', style: TextStyle( fontSize: 14, color: Colors.grey.shade500), ), ], ), ), ); } return ListView.builder( padding: const EdgeInsets.fromLTRB(16, 8, 16, 30), physics: const BouncingScrollPhysics(), itemCount: displayTenants.length, itemBuilder: (context, index) { final item = displayTenants[index]; return FadeTransition( opacity: _fadeAnimation, child: SlideTransition( position: _slideAnimation, child: _StoreListItem( item: item, index: index, imagePlaceholder: _imagePlaceholder(), onTap: () => _navigateTo(item), ), ), ); }, ); }), ), ], ), ); } } // ─── List item: banner image on top (outside card), info card below ────────── class _StoreListItem extends StatelessWidget { final dynamic item; final int index; final Widget imagePlaceholder; final VoidCallback onTap; const _StoreListItem({ required this.item, required this.index, required this.imagePlaceholder, required this.onTap, }); @override Widget build(BuildContext context) { final hasBanner = item.tenantbanner != null && (item.tenantbanner as String).isNotEmpty; return Padding( padding: const EdgeInsets.only(bottom: 20), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // ── Banner — tappable, rounded top corners, NO card background ─ GestureDetector( onTap: onTap, child: Hero( tag: 'store_${item.tenantid}_$index', child: ClipRRect( borderRadius: const BorderRadius.only( topLeft: Radius.circular(16), topRight: Radius.circular(16), ), child: Container( height: 180, width: double.infinity, color: Colors.grey.shade200, child: hasBanner ? Image.network( item.tenantbanner as String, fit: BoxFit.cover, loadingBuilder: (ctx, child, progress) => progress == null ? child : _ShimmerLoading(), errorBuilder: (ctx, _, __) => imagePlaceholder, ) : imagePlaceholder, ), ), ), ), // ── Info card — flush below the image ─────────────────────── _StoreInfoCard(item: item, onTap: onTap), ], ), ); } } // ─── Info card: name + location only ───────────────────────────────────────── class _StoreInfoCard extends StatefulWidget { final dynamic item; final VoidCallback onTap; const _StoreInfoCard({required this.item, required this.onTap}); @override State<_StoreInfoCard> createState() => _StoreInfoCardState(); } class _StoreInfoCardState extends State<_StoreInfoCard> with SingleTickerProviderStateMixin { late AnimationController _scaleController; late Animation _scaleAnimation; bool _isPressed = false; @override void initState() { super.initState(); _scaleController = AnimationController( vsync: this, duration: const Duration(milliseconds: 150), ); _scaleAnimation = Tween(begin: 1.0, end: 0.98).animate( CurvedAnimation(parent: _scaleController, curve: Curves.easeInOut), ); } @override void dispose() { _scaleController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return GestureDetector( onTapDown: (_) { setState(() => _isPressed = true); _scaleController.forward(); }, onTapUp: (_) { setState(() => _isPressed = false); _scaleController.reverse(); widget.onTap(); }, onTapCancel: () { setState(() => _isPressed = false); _scaleController.reverse(); }, child: ScaleTransition( scale: _scaleAnimation, child: AnimatedContainer( duration: const Duration(milliseconds: 150), padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), decoration: BoxDecoration( color: Colors.white, borderRadius: const BorderRadius.only( bottomLeft: Radius.circular(16), bottomRight: Radius.circular(16), ), boxShadow: [ BoxShadow( color: _isPressed ? Colors.black.withOpacity(0.04) : Colors.black.withOpacity(0.08), blurRadius: _isPressed ? 3 : 6, offset: Offset(0, _isPressed ? 1 : 3), ), ], ), child: Row( children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Store name ReusableTextWidget( text: widget.item.tenantname ?? '', fontFamily: FontConstants.fontFamily, fontSize: 16, fontWeight: FontWeight.bold, ), const SizedBox(height: 4), // Location Row( children: [ Icon(Icons.location_on_outlined, size: 13, color: Colors.grey.shade500), const SizedBox(width: 3), Expanded( child: ReusableTextWidget( text: widget.item.locationname ?? '', fontSize: 12, color: Colors.black54, ), ), ], ), ], ), ), Icon(Icons.arrow_forward_ios_rounded, size: 14, color: Colors.grey.shade400), ], ), ), ), ); } } // ─── Shimmer loading ────────────────────────────────────────────────────────── class _ShimmerLoading extends StatefulWidget { @override State<_ShimmerLoading> createState() => _ShimmerLoadingState(); } class _ShimmerLoadingState extends State<_ShimmerLoading> with SingleTickerProviderStateMixin { late AnimationController _ctrl; @override void initState() { super.initState(); _ctrl = AnimationController( vsync: this, duration: const Duration(milliseconds: 1500), )..repeat(); } @override void dispose() { _ctrl.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return AnimatedBuilder( animation: _ctrl, builder: (_, __) => Container( decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [ Colors.grey.shade200, Colors.grey.shade100, Colors.grey.shade200, ], stops: [ (_ctrl.value - 0.3).clamp(0.0, 1.0), _ctrl.value.clamp(0.0, 1.0), (_ctrl.value + 0.3).clamp(0.0, 1.0), ], ), ), ), ); } }