feat: Implement expandable tag sections with "Show More/Less" functionality on the agent home screen.

This commit is contained in:
pradeepkumar
2026-03-15 00:41:31 +05:30
parent 21f669fca5
commit b7f131c677
4 changed files with 59 additions and 19 deletions

View File

@@ -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));
} }

View File

@@ -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)),
), ),
),
], ],
); );
} }

View File

@@ -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.',
); );
} }

View File

@@ -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(