feat: Implement expandable tag sections with "Show More/Less" functionality on the agent home screen.
This commit is contained in:
@@ -212,6 +212,8 @@ class ApiClient {
|
|||||||
ApiConstants.authForgotPassword,
|
ApiConstants.authForgotPassword,
|
||||||
ApiConstants.authResetPassword,
|
ApiConstants.authResetPassword,
|
||||||
ApiConstants.authVerifyEmail,
|
ApiConstants.authVerifyEmail,
|
||||||
|
ApiConstants.twoFactorVerify,
|
||||||
|
ApiConstants.twoFactorVerifyBackup,
|
||||||
];
|
];
|
||||||
return authPaths.any((path) => url.contains(path));
|
return authPaths.any((path) => url.contains(path));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -89,6 +89,7 @@ class _AgentHomeContentState extends ConsumerState<_AgentHomeContent> {
|
|||||||
bool _isTogglingAvailability = false;
|
bool _isTogglingAvailability = false;
|
||||||
bool _showEmail = false;
|
bool _showEmail = false;
|
||||||
bool _showPhone = false;
|
bool _showPhone = false;
|
||||||
|
final Set<String> _expandedTagSections = {};
|
||||||
File? _localAvatarFile;
|
File? _localAvatarFile;
|
||||||
|
|
||||||
Future<void> _pickAndUploadAvatar() async {
|
Future<void> _pickAndUploadAvatar() async {
|
||||||
@@ -729,7 +730,7 @@ class _AgentHomeContentState extends ConsumerState<_AgentHomeContent> {
|
|||||||
color: AppColors.primaryDark)),
|
color: AppColors.primaryDark)),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
_buildTagsWrap(state.licensingAreas),
|
_buildTagsWrap(state.licensingAreas, sectionKey: 'licensing'),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
],
|
],
|
||||||
if (state.expertiseYears.isNotEmpty) ...[
|
if (state.expertiseYears.isNotEmpty) ...[
|
||||||
@@ -747,7 +748,7 @@ class _AgentHomeContentState extends ConsumerState<_AgentHomeContent> {
|
|||||||
.map((e) => e['years']!.isNotEmpty
|
.map((e) => e['years']!.isNotEmpty
|
||||||
? '${e['area']} – ${e['years']}'
|
? '${e['area']} – ${e['years']}'
|
||||||
: e['area']!)
|
: e['area']!)
|
||||||
.toList()),
|
.toList(), sectionKey: 'expertise'),
|
||||||
],
|
],
|
||||||
if (state.certifications.isNotEmpty) ...[
|
if (state.certifications.isNotEmpty) ...[
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
@@ -833,8 +834,9 @@ class _AgentHomeContentState extends ConsumerState<_AgentHomeContent> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildTagsWrap(List<String> tags) {
|
Widget _buildTagsWrap(List<String> tags, {required String sectionKey}) {
|
||||||
final displayTags = tags.take(6).toList();
|
final isExpanded = _expandedTagSections.contains(sectionKey);
|
||||||
|
final displayTags = isExpanded ? tags : tags.take(6).toList();
|
||||||
final remaining = tags.length - 6;
|
final remaining = tags.length - 6;
|
||||||
|
|
||||||
return Wrap(
|
return Wrap(
|
||||||
@@ -857,20 +859,33 @@ class _AgentHomeContentState extends ConsumerState<_AgentHomeContent> {
|
|||||||
color: AppColors.primaryDark)),
|
color: AppColors.primaryDark)),
|
||||||
)),
|
)),
|
||||||
if (remaining > 0)
|
if (remaining > 0)
|
||||||
Container(
|
GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
setState(() {
|
||||||
|
if (isExpanded) {
|
||||||
|
_expandedTagSections.remove(sectionKey);
|
||||||
|
} else {
|
||||||
|
_expandedTagSections.add(sectionKey);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
behavior: HitTestBehavior.opaque,
|
||||||
|
child: Container(
|
||||||
padding:
|
padding:
|
||||||
const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
borderRadius: BorderRadius.circular(15),
|
borderRadius: BorderRadius.circular(15),
|
||||||
border: Border.all(color: AppColors.primaryDark, width: 0.1),
|
border: Border.all(color: AppColors.primaryDark, width: 0.1),
|
||||||
),
|
),
|
||||||
child: Text('+$remaining More',
|
child: Text(
|
||||||
|
isExpanded ? 'Show Less' : '+$remaining More',
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontFamily: 'SourceSerif4',
|
fontFamily: 'SourceSerif4',
|
||||||
fontSize: 15,
|
fontSize: 15,
|
||||||
fontWeight: FontWeight.w300,
|
fontWeight: FontWeight.w300,
|
||||||
color: AppColors.primaryDark)),
|
color: AppColors.primaryDark)),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -216,7 +216,12 @@ class AuthNotifier extends StateNotifier<AuthState> {
|
|||||||
|
|
||||||
Future<void> verifyTwoFactor(String token) async {
|
Future<void> verifyTwoFactor(String token) async {
|
||||||
final tempToken = state.tempToken;
|
final tempToken = state.tempToken;
|
||||||
if (tempToken == null) return;
|
if (tempToken == null) {
|
||||||
|
state = state.copyWith(
|
||||||
|
errorMessage: 'Session expired. Please login again.',
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
state = state.copyWith(
|
state = state.copyWith(
|
||||||
status: AuthStatus.loading,
|
status: AuthStatus.loading,
|
||||||
@@ -235,13 +240,16 @@ class AuthNotifier extends StateNotifier<AuthState> {
|
|||||||
_connectSocket();
|
_connectSocket();
|
||||||
_initPushNotifications();
|
_initPushNotifications();
|
||||||
} on ApiException catch (e) {
|
} on ApiException catch (e) {
|
||||||
|
// Preserve tempToken and requiresTwoFactor so user can retry
|
||||||
state = state.copyWith(
|
state = state.copyWith(
|
||||||
status: AuthStatus.initial,
|
status: AuthStatus.initial,
|
||||||
|
requiresTwoFactor: true,
|
||||||
errorMessage: e.message,
|
errorMessage: e.message,
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
state = state.copyWith(
|
state = state.copyWith(
|
||||||
status: AuthStatus.initial,
|
status: AuthStatus.initial,
|
||||||
|
requiresTwoFactor: true,
|
||||||
errorMessage: 'Verification failed. Please try again.',
|
errorMessage: 'Verification failed. Please try again.',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -249,7 +257,12 @@ class AuthNotifier extends StateNotifier<AuthState> {
|
|||||||
|
|
||||||
Future<void> verifyBackupCode(String backupCode) async {
|
Future<void> verifyBackupCode(String backupCode) async {
|
||||||
final tempToken = state.tempToken;
|
final tempToken = state.tempToken;
|
||||||
if (tempToken == null) return;
|
if (tempToken == null) {
|
||||||
|
state = state.copyWith(
|
||||||
|
errorMessage: 'Session expired. Please login again.',
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
state = state.copyWith(
|
state = state.copyWith(
|
||||||
status: AuthStatus.loading,
|
status: AuthStatus.loading,
|
||||||
@@ -270,11 +283,13 @@ class AuthNotifier extends StateNotifier<AuthState> {
|
|||||||
} on ApiException catch (e) {
|
} on ApiException catch (e) {
|
||||||
state = state.copyWith(
|
state = state.copyWith(
|
||||||
status: AuthStatus.initial,
|
status: AuthStatus.initial,
|
||||||
|
requiresTwoFactor: true,
|
||||||
errorMessage: e.message,
|
errorMessage: e.message,
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
state = state.copyWith(
|
state = state.copyWith(
|
||||||
status: AuthStatus.initial,
|
status: AuthStatus.initial,
|
||||||
|
requiresTwoFactor: true,
|
||||||
errorMessage: 'Verification failed. Please try again.',
|
errorMessage: 'Verification failed. Please try again.',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ class _Verify2faScreenState extends ConsumerState<Verify2faScreen> {
|
|||||||
final List<FocusNode> _focusNodes = List.generate(6, (_) => FocusNode());
|
final List<FocusNode> _focusNodes = List.generate(6, (_) => FocusNode());
|
||||||
final TextEditingController _backupController = TextEditingController();
|
final TextEditingController _backupController = TextEditingController();
|
||||||
bool _useBackupCode = false;
|
bool _useBackupCode = false;
|
||||||
|
bool _isSubmitting = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
@@ -69,13 +70,17 @@ class _Verify2faScreenState extends ConsumerState<Verify2faScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _submit() {
|
void _submit() {
|
||||||
|
if (_isSubmitting) return;
|
||||||
|
|
||||||
if (_useBackupCode) {
|
if (_useBackupCode) {
|
||||||
final code = _backupController.text.trim();
|
final code = _backupController.text.trim();
|
||||||
if (code.isEmpty) return;
|
if (code.isEmpty) return;
|
||||||
|
_isSubmitting = true;
|
||||||
ref.read(authProvider.notifier).verifyBackupCode(code);
|
ref.read(authProvider.notifier).verifyBackupCode(code);
|
||||||
} else {
|
} else {
|
||||||
final code = _otpCode;
|
final code = _otpCode;
|
||||||
if (code.length != 6) return;
|
if (code.length != 6) return;
|
||||||
|
_isSubmitting = true;
|
||||||
ref.read(authProvider.notifier).verifyTwoFactor(code);
|
ref.read(authProvider.notifier).verifyTwoFactor(code);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -90,11 +95,14 @@ class _Verify2faScreenState extends ConsumerState<Verify2faScreen> {
|
|||||||
final authState = ref.watch(authProvider);
|
final authState = ref.watch(authProvider);
|
||||||
final isLoading = authState.status == AuthStatus.loading;
|
final isLoading = authState.status == AuthStatus.loading;
|
||||||
|
|
||||||
// Navigate to home when authentication succeeds
|
// Navigate to home when authentication succeeds, reset submit lock on error
|
||||||
ref.listen<AuthState>(authProvider, (previous, next) {
|
ref.listen<AuthState>(authProvider, (previous, next) {
|
||||||
if (next.status == AuthStatus.authenticated) {
|
if (next.status == AuthStatus.authenticated) {
|
||||||
context.go('/home');
|
context.go('/home');
|
||||||
}
|
}
|
||||||
|
if (next.status != AuthStatus.loading) {
|
||||||
|
_isSubmitting = false;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
|
|||||||
Reference in New Issue
Block a user