feat: implement debounced live search with clear button, refactor UI components, and adjust layout spacing and image cropping across screens.
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
@@ -31,6 +33,7 @@ class AgentSearchScreen extends ConsumerStatefulWidget {
|
||||
class _AgentSearchScreenState extends ConsumerState<AgentSearchScreen> {
|
||||
late TextEditingController _searchController;
|
||||
final ScrollController _scrollController = ScrollController();
|
||||
Timer? _searchDebounce;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -67,11 +70,22 @@ class _AgentSearchScreenState extends ConsumerState<AgentSearchScreen> {
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_searchDebounce?.cancel();
|
||||
_searchController.dispose();
|
||||
_scrollController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
/// Debounced live search — fires search() while the user types so results
|
||||
/// update incrementally. Empty value resets to the default listing.
|
||||
void _onSearchChanged(String value) {
|
||||
_searchDebounce?.cancel();
|
||||
_searchDebounce = Timer(const Duration(milliseconds: 300), () {
|
||||
if (!mounted) return;
|
||||
ref.read(searchAgentsProvider.notifier).search(value.trim());
|
||||
});
|
||||
}
|
||||
|
||||
void _onScroll() {
|
||||
if (_scrollController.position.pixels >=
|
||||
_scrollController.position.maxScrollExtent - 200) {
|
||||
@@ -185,10 +199,35 @@ class _AgentSearchScreenState extends ConsumerState<AgentSearchScreen> {
|
||||
horizontal: 14,
|
||||
vertical: 10,
|
||||
),
|
||||
// Clear (×) button — resets the listing instantly.
|
||||
suffixIcon: _searchController.text.isNotEmpty
|
||||
? IconButton(
|
||||
icon: const Icon(
|
||||
Icons.close,
|
||||
size: 18,
|
||||
color: AppColors.hintText,
|
||||
),
|
||||
onPressed: () {
|
||||
_searchDebounce?.cancel();
|
||||
_searchController.clear();
|
||||
setState(() {});
|
||||
ref
|
||||
.read(searchAgentsProvider.notifier)
|
||||
.search('');
|
||||
},
|
||||
)
|
||||
: null,
|
||||
),
|
||||
onChanged: (value) {
|
||||
// Toggle suffix-icon visibility on each keystroke.
|
||||
setState(() {});
|
||||
_onSearchChanged(value);
|
||||
},
|
||||
onSubmitted: (value) {
|
||||
if (value.trim().isEmpty) return;
|
||||
ref.read(searchAgentsProvider.notifier).search(value);
|
||||
_searchDebounce?.cancel();
|
||||
ref
|
||||
.read(searchAgentsProvider.notifier)
|
||||
.search(value.trim());
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
@@ -29,7 +29,7 @@ class _ContactScreenState extends State<ContactScreen> {
|
||||
String _contactOfficeAddress = '123 Market Street';
|
||||
String _contactOfficeCity = 'New York CA 234737';
|
||||
String _pageTitle = 'Get In Touch';
|
||||
String _pageDescription = 'Have a question about a property or need assistance? Fill out the form below and our team will get back to you shortly.';
|
||||
String _pageDescription = 'Need assistance? Fill out the form below and our team will get back to you shortly.';
|
||||
String _directionsUrl = 'https://maps.google.com/?q=123+Market+Street+New+York+CA+234737';
|
||||
|
||||
@override
|
||||
|
||||
@@ -375,46 +375,35 @@ class _MenuDrawer extends ConsumerWidget {
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
height: 54,
|
||||
child: ElevatedButton(
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
context.push('/login');
|
||||
},
|
||||
icon: const Icon(
|
||||
Icons.login,
|
||||
color: Colors.white,
|
||||
size: 20,
|
||||
),
|
||||
label: const Text(
|
||||
'Log In',
|
||||
style: TextStyle(
|
||||
fontFamily: 'Fractul',
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.accentOrange,
|
||||
foregroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(15),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 20,
|
||||
width: 20,
|
||||
child: Center(
|
||||
child: Icon(
|
||||
Icons.login,
|
||||
color: Colors.white,
|
||||
size: 18,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
const Text(
|
||||
'Log In',
|
||||
style: TextStyle(
|
||||
fontFamily: 'Fractul',
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
@@ -105,9 +105,10 @@ class _TopProfessionalsSectionState
|
||||
_buildEmptyState(_activeTab)
|
||||
else ...[
|
||||
// Professional Cards - horizontal scroll
|
||||
// Use screen-relative height so cards fit on all devices
|
||||
// Use screen-relative height so cards fit on all devices.
|
||||
// Bumped to 0.72 to fit the taller image (300) + content area.
|
||||
SizedBox(
|
||||
height: MediaQuery.of(context).size.height * 0.64,
|
||||
height: MediaQuery.of(context).size.height * 0.72,
|
||||
child: ListView.builder(
|
||||
// Unique key per tab forces Flutter to rebuild (not reuse)
|
||||
// the list items, preventing stale images from the other tab.
|
||||
@@ -318,10 +319,12 @@ class _TopProfessionalsSectionState
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Image (226px per Figma)
|
||||
// Image — taller box for portrait photos so faces aren't cropped.
|
||||
// Uses topCenter alignment as a fallback when the photo is much
|
||||
// taller than the box (crops bottom, keeps the face visible).
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
height: 226,
|
||||
height: 300,
|
||||
child: ClipRRect(
|
||||
borderRadius: const BorderRadius.vertical(
|
||||
top: Radius.circular(15),
|
||||
@@ -334,9 +337,9 @@ class _TopProfessionalsSectionState
|
||||
key: ValueKey('${_activeTab}_$imageUrl'),
|
||||
imageUrl: imageUrl,
|
||||
width: double.infinity,
|
||||
height: 226,
|
||||
height: 300,
|
||||
fit: BoxFit.cover,
|
||||
alignment: Alignment.topCenter,
|
||||
alignment: const Alignment(0, -0.4),
|
||||
errorWidget: (_) => _buildImagePlaceholder(index),
|
||||
)
|
||||
: _buildImagePlaceholder(index),
|
||||
@@ -569,7 +572,7 @@ class _TopProfessionalsSectionState
|
||||
children: [
|
||||
// Image placeholder
|
||||
Container(
|
||||
height: 226,
|
||||
height: 300,
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(15)),
|
||||
|
||||
@@ -23,7 +23,7 @@ class _OnboardingScreenState extends State<OnboardingScreen> {
|
||||
iconHeight: 100,
|
||||
title: 'Connect with Top Professionals',
|
||||
subtitle:
|
||||
'Professionals can list properties and find qualified buyers faster than ever.',
|
||||
'Agents can highlight their specializations for buyers to find faster than ever',
|
||||
),
|
||||
_OnboardingData(
|
||||
bgAsset: 'assets/images/onboarding_2_bg.svg',
|
||||
@@ -32,7 +32,7 @@ class _OnboardingScreenState extends State<OnboardingScreen> {
|
||||
iconHeight: 75,
|
||||
title: 'Real help. Real professionals. Instantly.',
|
||||
subtitle:
|
||||
'Chat with professionals and get answers about properties instantly.',
|
||||
'Chat with agents instantly and find your service provider with ease',
|
||||
),
|
||||
];
|
||||
|
||||
|
||||
Reference in New Issue
Block a user