900 lines
32 KiB
Dart
900 lines
32 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:flutter_svg/flutter_svg.dart';
|
|
import 'package:go_router/go_router.dart';
|
|
import 'package:shimmer/shimmer.dart';
|
|
import 'package:real_estate_mobile/core/constants/app_colors.dart';
|
|
import 'package:real_estate_mobile/core/widgets/s3_image.dart';
|
|
import 'package:real_estate_mobile/features/agents/data/models/agent_profile.dart';
|
|
import 'package:real_estate_mobile/features/agents/data/models/agent_type.dart';
|
|
import 'package:real_estate_mobile/features/agents/presentation/providers/search_agents_provider.dart';
|
|
import 'package:real_estate_mobile/features/agents/presentation/widgets/agent_filter_sheet.dart';
|
|
|
|
class AgentSearchScreen extends ConsumerStatefulWidget {
|
|
final String? initialQuery;
|
|
final String? initialAgentTypeId;
|
|
final String? initialLocation;
|
|
final String? initialCategory;
|
|
|
|
const AgentSearchScreen({
|
|
super.key,
|
|
this.initialQuery,
|
|
this.initialAgentTypeId,
|
|
this.initialLocation,
|
|
this.initialCategory,
|
|
});
|
|
|
|
@override
|
|
ConsumerState<AgentSearchScreen> createState() => _AgentSearchScreenState();
|
|
}
|
|
|
|
class _AgentSearchScreenState extends ConsumerState<AgentSearchScreen> {
|
|
late TextEditingController _searchController;
|
|
final ScrollController _scrollController = ScrollController();
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_searchController = TextEditingController(text: widget.initialQuery ?? '');
|
|
_scrollController.addListener(_onScroll);
|
|
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
final notifier = ref.read(searchAgentsProvider.notifier);
|
|
|
|
// Apply initial agent type filter if provided
|
|
if (widget.initialAgentTypeId != null) {
|
|
notifier.filterByAgentType(widget.initialAgentTypeId);
|
|
}
|
|
|
|
// Apply location and category filters if provided
|
|
final initialFilters = <String, List<String>>{};
|
|
if (widget.initialLocation != null) {
|
|
initialFilters['state'] = [widget.initialLocation!];
|
|
}
|
|
if (widget.initialCategory != null) {
|
|
initialFilters['specialization'] = [widget.initialCategory!];
|
|
}
|
|
if (initialFilters.isNotEmpty) {
|
|
notifier.applyFilters(initialFilters);
|
|
}
|
|
|
|
// Trigger initial search if query provided
|
|
if (widget.initialQuery != null && widget.initialQuery!.isNotEmpty) {
|
|
notifier.search(widget.initialQuery!);
|
|
}
|
|
});
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_searchController.dispose();
|
|
_scrollController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
void _onScroll() {
|
|
if (_scrollController.position.pixels >=
|
|
_scrollController.position.maxScrollExtent - 200) {
|
|
ref.read(searchAgentsProvider.notifier).loadMore();
|
|
}
|
|
}
|
|
|
|
void _openFilterSheet() {
|
|
final state = ref.read(searchAgentsProvider);
|
|
showModalBottomSheet(
|
|
context: context,
|
|
isScrollControlled: true,
|
|
backgroundColor: Colors.transparent,
|
|
builder: (_) => AgentFilterSheet(
|
|
filterFields: state.filterFields,
|
|
agentTypes: state.agentTypes,
|
|
currentFilters: state.activeFilters,
|
|
selectedAgentTypeId: state.selectedAgentTypeId,
|
|
onApply: (filters) {
|
|
ref.read(searchAgentsProvider.notifier).applyFilters(filters);
|
|
},
|
|
onClearAll: () {
|
|
ref.read(searchAgentsProvider.notifier).clearFilters();
|
|
},
|
|
onListingTypeChange: (agentTypeId) {
|
|
ref
|
|
.read(searchAgentsProvider.notifier)
|
|
.filterByAgentType(agentTypeId);
|
|
},
|
|
),
|
|
);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final state = ref.watch(searchAgentsProvider);
|
|
|
|
return Column(
|
|
children: [
|
|
_buildSearchBar(),
|
|
const SizedBox(height: 12),
|
|
_buildFilterChips(state.agentTypes, state.selectedAgentTypeId),
|
|
const SizedBox(height: 12),
|
|
Expanded(child: _buildAgentList(state)),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildSearchBar() {
|
|
return Padding(
|
|
padding: const EdgeInsets.fromLTRB(16, 16, 24, 0),
|
|
child: SizedBox(
|
|
height: 42,
|
|
child: Row(
|
|
children: [
|
|
GestureDetector(
|
|
onTap: () {
|
|
if (Navigator.of(context).canPop()) {
|
|
Navigator.of(context).pop();
|
|
} else {
|
|
context.go('/home');
|
|
}
|
|
},
|
|
child: const Padding(
|
|
padding: EdgeInsets.only(right: 8),
|
|
child: Icon(
|
|
Icons.arrow_back_ios_new_rounded,
|
|
color: AppColors.primaryDark,
|
|
size: 20,
|
|
),
|
|
),
|
|
),
|
|
Expanded(
|
|
child: Container(
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
border: Border.all(
|
|
color: AppColors.primaryDark,
|
|
width: 0.1,
|
|
),
|
|
borderRadius: BorderRadius.circular(7),
|
|
),
|
|
child: Theme(
|
|
data: Theme.of(context).copyWith(
|
|
inputDecorationTheme: const InputDecorationTheme(
|
|
filled: false,
|
|
border: InputBorder.none,
|
|
),
|
|
),
|
|
child: TextField(
|
|
controller: _searchController,
|
|
style: const TextStyle(
|
|
fontFamily: 'Fractul',
|
|
fontSize: 14,
|
|
fontWeight: FontWeight.w500,
|
|
color: AppColors.primaryDark,
|
|
),
|
|
decoration: InputDecoration(
|
|
hintText: 'Search Agents',
|
|
hintStyle: TextStyle(
|
|
fontFamily: 'Fractul',
|
|
fontSize: 14,
|
|
fontWeight: FontWeight.w500,
|
|
color: Colors.black.withValues(alpha: 0.5),
|
|
),
|
|
filled: false,
|
|
border: InputBorder.none,
|
|
enabledBorder: InputBorder.none,
|
|
focusedBorder: InputBorder.none,
|
|
contentPadding: const EdgeInsets.symmetric(
|
|
horizontal: 14,
|
|
vertical: 10,
|
|
),
|
|
),
|
|
onSubmitted: (value) {
|
|
ref.read(searchAgentsProvider.notifier).search(value);
|
|
},
|
|
),
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(width: 0),
|
|
GestureDetector(
|
|
onTap: () {
|
|
ref
|
|
.read(searchAgentsProvider.notifier)
|
|
.search(_searchController.text);
|
|
},
|
|
child: Container(
|
|
width: 62,
|
|
height: 42,
|
|
decoration: BoxDecoration(
|
|
color: AppColors.accentOrange,
|
|
border: Border.all(
|
|
color: AppColors.primaryDark,
|
|
width: 0.1,
|
|
),
|
|
borderRadius: BorderRadius.circular(7),
|
|
),
|
|
child: Center(
|
|
child: SvgPicture.string(
|
|
'<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M15.5 14h-.79l-.28-.27A6.471 6.471 0 0 0 16 9.5 6.5 6.5 0 1 0 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z" fill="white"/></svg>',
|
|
width: 24,
|
|
height: 24,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildFilterChips(
|
|
List<AgentType> agentTypes, String? selectedTypeId) {
|
|
return SizedBox(
|
|
height: 32,
|
|
child: Padding(
|
|
padding: const EdgeInsets.only(left: 24),
|
|
child: Row(
|
|
children: [
|
|
Expanded(
|
|
child: ListView.separated(
|
|
scrollDirection: Axis.horizontal,
|
|
itemCount: agentTypes.length,
|
|
separatorBuilder: (_, __) => const SizedBox(width: 4),
|
|
itemBuilder: (context, index) {
|
|
final type = agentTypes[index];
|
|
final isSelected = type.id == selectedTypeId;
|
|
return GestureDetector(
|
|
onTap: () {
|
|
ref
|
|
.read(searchAgentsProvider.notifier)
|
|
.filterByAgentType(
|
|
isSelected ? null : type.id);
|
|
},
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 12, vertical: 4),
|
|
decoration: BoxDecoration(
|
|
color: isSelected
|
|
? AppColors.accentOrange
|
|
: Colors.transparent,
|
|
borderRadius: BorderRadius.circular(15),
|
|
border: Border.all(
|
|
color: AppColors.primaryDark,
|
|
width: 0.1,
|
|
),
|
|
),
|
|
child: Center(
|
|
child: Text(
|
|
type.name,
|
|
style: TextStyle(
|
|
fontFamily: 'SourceSerif4',
|
|
fontSize: 14,
|
|
fontWeight: FontWeight.w400,
|
|
color: isSelected
|
|
? Colors.white
|
|
: AppColors.primaryDark,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
const SizedBox(width: 8),
|
|
Padding(
|
|
padding: const EdgeInsets.only(right: 24),
|
|
child: GestureDetector(
|
|
onTap: () => _openFilterSheet(),
|
|
child: Container(
|
|
width: 39,
|
|
height: 27,
|
|
decoration: BoxDecoration(
|
|
borderRadius: BorderRadius.circular(15),
|
|
border: Border.all(
|
|
color: AppColors.primaryDark,
|
|
width: 0.1,
|
|
),
|
|
),
|
|
child: const Icon(
|
|
Icons.tune,
|
|
color: AppColors.primaryDark,
|
|
size: 20,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildShimmerList() {
|
|
return Shimmer.fromColors(
|
|
baseColor: const Color(0xFFE8E8E8),
|
|
highlightColor: const Color(0xFFF5F5F5),
|
|
child: ListView.builder(
|
|
padding: const EdgeInsets.symmetric(horizontal: 24),
|
|
physics: const NeverScrollableScrollPhysics(),
|
|
itemCount: 3,
|
|
itemBuilder: (_, __) => Padding(
|
|
padding: const EdgeInsets.only(bottom: 16),
|
|
child: Container(
|
|
height: 300,
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(15),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildAgentList(SearchAgentsState state) {
|
|
if (state.isLoading) {
|
|
return _buildShimmerList();
|
|
}
|
|
|
|
if (state.error != null) {
|
|
return Center(
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
const Icon(Icons.error_outline,
|
|
color: AppColors.accentOrange, size: 48),
|
|
const SizedBox(height: 12),
|
|
Text(
|
|
state.error!,
|
|
style: const TextStyle(
|
|
fontFamily: 'Fractul',
|
|
fontSize: 14,
|
|
color: AppColors.primaryDark,
|
|
),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
const SizedBox(height: 16),
|
|
TextButton(
|
|
onPressed: () =>
|
|
ref.read(searchAgentsProvider.notifier).refresh(),
|
|
child: const Text(
|
|
'Retry',
|
|
style: TextStyle(
|
|
fontFamily: 'Fractul',
|
|
fontSize: 14,
|
|
fontWeight: FontWeight.w600,
|
|
color: AppColors.accentOrange,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
if (state.agents.isEmpty) {
|
|
return const Center(
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Icon(Icons.people_outline, color: AppColors.hintText, size: 48),
|
|
SizedBox(height: 12),
|
|
Text(
|
|
'No agents found',
|
|
style: TextStyle(
|
|
fontFamily: 'Fractul',
|
|
fontSize: 14,
|
|
color: AppColors.hintText,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
return ListView.builder(
|
|
controller: _scrollController,
|
|
padding: const EdgeInsets.symmetric(horizontal: 24),
|
|
itemCount: state.agents.length + (state.isLoadingMore ? 1 : 0),
|
|
itemBuilder: (context, index) {
|
|
if (index == state.agents.length) {
|
|
return const Padding(
|
|
padding: EdgeInsets.symmetric(vertical: 20),
|
|
child: Center(
|
|
child:
|
|
CircularProgressIndicator(color: AppColors.accentOrange),
|
|
),
|
|
);
|
|
}
|
|
return Padding(
|
|
padding: const EdgeInsets.only(bottom: 16),
|
|
child: GestureDetector(
|
|
onTap: () => context.push('/agents/detail/${state.agents[index].id}'),
|
|
child: _AgentCard(agent: state.agents[index]),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
}
|
|
|
|
// --- Agent Card Widget matching Figma design ---
|
|
|
|
class _AgentCard extends StatefulWidget {
|
|
final AgentProfile agent;
|
|
|
|
const _AgentCard({required this.agent});
|
|
|
|
@override
|
|
State<_AgentCard> createState() => _AgentCardState();
|
|
}
|
|
|
|
class _AgentCardState extends State<_AgentCard> {
|
|
bool _bioExpanded = false;
|
|
bool _tagsExpanded = false;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final agent = widget.agent;
|
|
final specializations = agent.specializations;
|
|
final bio = agent.description;
|
|
final matchPercent = agent.matchPercentage != null
|
|
? '${agent.matchPercentage}%'
|
|
: null;
|
|
|
|
return Container(
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(20),
|
|
border: Border.all(color: AppColors.primaryDark.withValues(alpha: 0.1)),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: const Color(0xFFD9D9D9).withValues(alpha: 0.5),
|
|
offset: const Offset(0, 10),
|
|
blurRadius: 20,
|
|
),
|
|
],
|
|
),
|
|
clipBehavior: Clip.antiAlias,
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
// Cover image with match badge
|
|
Stack(
|
|
children: [
|
|
S3Image(
|
|
imageUrl: agent.avatarUrl,
|
|
width: double.infinity,
|
|
height: 265,
|
|
alignment: Alignment.topCenter,
|
|
errorWidget: (_) => Container(
|
|
width: double.infinity,
|
|
height: 265,
|
|
color: AppColors.gradientStart,
|
|
child: const Icon(Icons.person, size: 80, color: AppColors.primaryDark),
|
|
),
|
|
),
|
|
if (matchPercent != null)
|
|
Positioned(
|
|
top: 0,
|
|
right: 0,
|
|
child: Container(
|
|
padding:
|
|
const EdgeInsets.symmetric(horizontal: 10, vertical: 12),
|
|
decoration: const BoxDecoration(
|
|
color: Color(0xFF638559),
|
|
borderRadius: BorderRadius.only(
|
|
bottomLeft: Radius.circular(20),
|
|
),
|
|
),
|
|
child: Text(
|
|
matchPercent,
|
|
style: const TextStyle(
|
|
fontFamily: 'SourceSerif4',
|
|
fontSize: 14,
|
|
fontWeight: FontWeight.w400,
|
|
color: Color(0xFFF0F5FC),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
|
|
// Card content
|
|
Padding(
|
|
padding: const EdgeInsets.fromLTRB(34, 16, 20, 20),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
// Name
|
|
Text.rich(
|
|
TextSpan(
|
|
children: [
|
|
TextSpan(
|
|
text: agent.firstName,
|
|
style: const TextStyle(
|
|
fontFamily: 'Fractul',
|
|
fontSize: 15,
|
|
fontWeight: FontWeight.w700,
|
|
color: AppColors.primaryDark,
|
|
),
|
|
),
|
|
TextSpan(
|
|
text: ' ${agent.lastName}',
|
|
style: const TextStyle(
|
|
fontFamily: 'Fractul',
|
|
fontSize: 15,
|
|
fontWeight: FontWeight.w400,
|
|
color: AppColors.primaryDark,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
// Verified badge
|
|
if (agent.isVerified) ...[
|
|
const SizedBox(height: 4),
|
|
Row(
|
|
children: [
|
|
SvgPicture.asset(
|
|
'assets/icons/verified_badge_icon.svg',
|
|
width: 16,
|
|
height: 16,
|
|
placeholderBuilder: (_) => const Icon(
|
|
Icons.verified,
|
|
color: Color(0xFF5D8B5A),
|
|
size: 16,
|
|
),
|
|
),
|
|
const SizedBox(width: 4),
|
|
const Text(
|
|
'(Verified Expert)',
|
|
style: TextStyle(
|
|
fontFamily: 'SourceSerif4',
|
|
fontSize: 14,
|
|
fontWeight: FontWeight.w500,
|
|
color: Color(0xFF5D8B5A),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
const SizedBox(height: 4),
|
|
|
|
// Headline / title
|
|
Text(
|
|
agent.agentType?.name ?? agent.headline ?? 'Professional',
|
|
style: const TextStyle(
|
|
fontFamily: 'Fractul',
|
|
fontSize: 15,
|
|
fontWeight: FontWeight.w400,
|
|
color: AppColors.primaryDark,
|
|
),
|
|
),
|
|
const SizedBox(height: 10),
|
|
|
|
// Location + Member Since row
|
|
Row(
|
|
children: [
|
|
if (agent.location.isNotEmpty) ...[
|
|
SvgPicture.asset(
|
|
'assets/icons/location_pin_icon.svg',
|
|
width: 14,
|
|
height: 14,
|
|
placeholderBuilder: (_) => const Icon(
|
|
Icons.location_on,
|
|
color: AppColors.accentOrange,
|
|
size: 14,
|
|
),
|
|
),
|
|
const SizedBox(width: 4),
|
|
Builder(builder: (_) {
|
|
final allParts = <String>[];
|
|
// Extract states first, then cities (state-first display)
|
|
List<String> extractValues(dynamic fv) {
|
|
final out = <String>[];
|
|
if (fv.jsonValue is List) {
|
|
for (final v in (fv.jsonValue as List)) {
|
|
final s = v.toString().trim();
|
|
if (s.isEmpty) continue;
|
|
out.add(s
|
|
.split('_')
|
|
.map((w) => w.isNotEmpty
|
|
? '${w[0].toUpperCase()}${w.substring(1).toLowerCase()}'
|
|
: '')
|
|
.join(' '));
|
|
}
|
|
} else if (fv.textValue != null &&
|
|
(fv.textValue as String).isNotEmpty) {
|
|
out.add(fv.textValue as String);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
void addUnique(String value) {
|
|
if (!allParts.any((p) =>
|
|
p.toLowerCase() == value.toLowerCase())) {
|
|
allParts.add(value);
|
|
}
|
|
}
|
|
|
|
// 1. States first
|
|
for (final fv in agent.fieldValues) {
|
|
final slug = fv.fieldSlug.toLowerCase();
|
|
if (slug == 'state' || slug == 'state_name') {
|
|
for (final v in extractValues(fv)) {
|
|
addUnique(v);
|
|
}
|
|
}
|
|
}
|
|
// 2. Cities next
|
|
for (final fv in agent.fieldValues) {
|
|
final slug = fv.fieldSlug.toLowerCase();
|
|
if (slug == 'city' || slug == 'city_name') {
|
|
for (final v in extractValues(fv)) {
|
|
addUnique(v);
|
|
}
|
|
}
|
|
}
|
|
// Fallback to agent.location + serviceAreas
|
|
if (allParts.isEmpty) {
|
|
final locParts = agent.location
|
|
.split(',')
|
|
.map((s) => s.trim())
|
|
.where((s) => s.isNotEmpty);
|
|
allParts.addAll(locParts);
|
|
for (final area in agent.serviceAreas) {
|
|
final trimmed = area.trim();
|
|
if (trimmed.isNotEmpty &&
|
|
!allParts.any((p) =>
|
|
p.toLowerCase() ==
|
|
trimmed.toLowerCase())) {
|
|
allParts.add(trimmed);
|
|
}
|
|
}
|
|
}
|
|
final firstPart = allParts.isNotEmpty
|
|
? allParts.first
|
|
: agent.location;
|
|
final remaining = allParts.length - 1;
|
|
|
|
return Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
ConstrainedBox(
|
|
constraints: const BoxConstraints(maxWidth: 90),
|
|
child: Text(
|
|
firstPart,
|
|
style: const TextStyle(
|
|
fontFamily: 'SourceSerif4',
|
|
fontSize: 14,
|
|
fontWeight: FontWeight.w500,
|
|
color: AppColors.primaryDark,
|
|
),
|
|
overflow: TextOverflow.ellipsis,
|
|
maxLines: 1,
|
|
),
|
|
),
|
|
if (remaining > 0)
|
|
GestureDetector(
|
|
onTap: () =>
|
|
_showLocationModal(context, allParts),
|
|
child: Text(
|
|
' +$remaining more',
|
|
style: const TextStyle(
|
|
fontFamily: 'SourceSerif4',
|
|
fontSize: 14,
|
|
fontWeight: FontWeight.w500,
|
|
color: AppColors.accentOrange,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}),
|
|
const SizedBox(width: 16),
|
|
],
|
|
SvgPicture.asset(
|
|
'assets/icons/calendar_icon.svg',
|
|
width: 14,
|
|
height: 14,
|
|
placeholderBuilder: (_) => const Icon(
|
|
Icons.calendar_today,
|
|
color: AppColors.accentOrange,
|
|
size: 14,
|
|
),
|
|
),
|
|
const SizedBox(width: 4),
|
|
Flexible(
|
|
child: Text(
|
|
agent.memberSinceText,
|
|
style: const TextStyle(
|
|
fontFamily: 'SourceSerif4',
|
|
fontSize: 14,
|
|
fontWeight: FontWeight.w500,
|
|
color: AppColors.primaryDark,
|
|
),
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 12),
|
|
|
|
// Expertise tags
|
|
if (specializations.isNotEmpty) ...[
|
|
Wrap(
|
|
alignment: WrapAlignment.start,
|
|
spacing: 6,
|
|
runSpacing: 6,
|
|
children: [
|
|
...(_tagsExpanded
|
|
? specializations
|
|
: specializations.take(6))
|
|
.map((tag) => _buildTag(tag)),
|
|
if (specializations.length > 6)
|
|
GestureDetector(
|
|
onTap: () =>
|
|
setState(() => _tagsExpanded = !_tagsExpanded),
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 10, vertical: 5),
|
|
decoration: BoxDecoration(
|
|
borderRadius: BorderRadius.circular(20),
|
|
color: AppColors.accentOrange
|
|
.withValues(alpha: 0.1),
|
|
border: Border.all(
|
|
color: AppColors.accentOrange, width: 0.7),
|
|
),
|
|
child: Text(
|
|
_tagsExpanded
|
|
? 'Show Less'
|
|
: '+${specializations.length - 6} more',
|
|
style: const TextStyle(
|
|
fontFamily: 'SourceSerif4',
|
|
fontSize: 13,
|
|
fontWeight: FontWeight.w600,
|
|
color: AppColors.accentOrange,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 14),
|
|
],
|
|
|
|
// Bio
|
|
if (bio.isNotEmpty) ...[
|
|
Text.rich(
|
|
TextSpan(
|
|
children: [
|
|
TextSpan(
|
|
text: _bioExpanded || bio.length <= 150
|
|
? bio
|
|
: '${bio.substring(0, 150)}... ',
|
|
style: const TextStyle(
|
|
fontFamily: 'SourceSerif4',
|
|
fontSize: 10,
|
|
fontWeight: FontWeight.w400,
|
|
color: AppColors.primaryDark,
|
|
),
|
|
),
|
|
if (bio.length > 150)
|
|
WidgetSpan(
|
|
child: GestureDetector(
|
|
onTap: () =>
|
|
setState(() => _bioExpanded = !_bioExpanded),
|
|
child: Text(
|
|
_bioExpanded ? 'Show Less.' : 'Show More.',
|
|
style: const TextStyle(
|
|
fontFamily: 'Fractul',
|
|
fontSize: 10,
|
|
fontWeight: FontWeight.w600,
|
|
color: AppColors.primaryDark,
|
|
decoration: TextDecoration.underline,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
|
|
void _showLocationModal(BuildContext context, List<String> parts) {
|
|
showModalBottomSheet(
|
|
context: context,
|
|
shape: const RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
|
),
|
|
backgroundColor: Colors.white,
|
|
builder: (_) => Padding(
|
|
padding: const EdgeInsets.all(20),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
const Text(
|
|
'All Locations',
|
|
style: TextStyle(
|
|
fontFamily: 'Fractul',
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.w700,
|
|
color: AppColors.primaryDark,
|
|
),
|
|
),
|
|
GestureDetector(
|
|
onTap: () => Navigator.pop(context),
|
|
child: const Icon(Icons.close, size: 20),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 16),
|
|
Wrap(
|
|
spacing: 8,
|
|
runSpacing: 8,
|
|
children: parts
|
|
.map((p) => Container(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 12, vertical: 6),
|
|
decoration: BoxDecoration(
|
|
borderRadius: BorderRadius.circular(15),
|
|
border: Border.all(
|
|
color: AppColors.primaryDark.withValues(alpha: 0.2),
|
|
),
|
|
),
|
|
child: Text(
|
|
p,
|
|
style: const TextStyle(
|
|
fontFamily: 'SourceSerif4',
|
|
fontSize: 14,
|
|
fontWeight: FontWeight.w500,
|
|
color: AppColors.primaryDark,
|
|
),
|
|
),
|
|
))
|
|
.toList(),
|
|
),
|
|
const SizedBox(height: 20),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildTag(String label) {
|
|
return Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 5),
|
|
decoration: BoxDecoration(
|
|
borderRadius: BorderRadius.circular(20),
|
|
border: Border.all(color: AppColors.primaryDark, width: 0.7),
|
|
),
|
|
child: Text(
|
|
label,
|
|
style: const TextStyle(
|
|
fontFamily: 'SourceSerif4',
|
|
fontSize: 13,
|
|
fontWeight: FontWeight.w400,
|
|
color: AppColors.primaryDark,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|