Initial commit: unionflow-mobile-apps
Application Flutter complète (sans build artifacts). Signed-off-by: lions dev Team
This commit is contained in:
@@ -0,0 +1,348 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../../../shared/design_system/unionflow_design_system.dart';
|
||||
import '../../../../members/presentation/pages/members_page_wrapper.dart';
|
||||
import '../../../../events/presentation/pages/events_page_wrapper.dart';
|
||||
import '../../../../contributions/presentation/pages/contributions_page_wrapper.dart';
|
||||
import '../../../../reports/presentation/pages/reports_page_wrapper.dart';
|
||||
import '../../../../settings/presentation/pages/system_settings_page.dart';
|
||||
|
||||
/// Widget de recherche rapide pour le dashboard
|
||||
class DashboardSearchWidget extends StatefulWidget {
|
||||
final Function(String)? onSearch;
|
||||
final String? hintText;
|
||||
final List<SearchSuggestion>? suggestions;
|
||||
|
||||
const DashboardSearchWidget({
|
||||
super.key,
|
||||
this.onSearch,
|
||||
this.hintText,
|
||||
this.suggestions,
|
||||
});
|
||||
|
||||
@override
|
||||
State<DashboardSearchWidget> createState() => _DashboardSearchWidgetState();
|
||||
}
|
||||
|
||||
class _DashboardSearchWidgetState extends State<DashboardSearchWidget>
|
||||
with TickerProviderStateMixin {
|
||||
final TextEditingController _searchController = TextEditingController();
|
||||
final FocusNode _focusNode = FocusNode();
|
||||
late AnimationController _animationController;
|
||||
late Animation<double> _scaleAnimation;
|
||||
bool _isExpanded = false;
|
||||
List<SearchSuggestion> _filteredSuggestions = [];
|
||||
List<SearchSuggestion>? _defaultSuggestions;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_setupAnimations();
|
||||
_setupListeners();
|
||||
_filteredSuggestions = widget.suggestions ?? [];
|
||||
}
|
||||
|
||||
void _setupAnimations() {
|
||||
_animationController = AnimationController(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
vsync: this,
|
||||
);
|
||||
|
||||
_scaleAnimation = Tween<double>(
|
||||
begin: 1.0,
|
||||
end: 1.05,
|
||||
).animate(CurvedAnimation(
|
||||
parent: _animationController,
|
||||
curve: Curves.easeInOut,
|
||||
));
|
||||
}
|
||||
|
||||
void _setupListeners() {
|
||||
_focusNode.addListener(() {
|
||||
setState(() {
|
||||
_isExpanded = _focusNode.hasFocus;
|
||||
});
|
||||
|
||||
if (_focusNode.hasFocus) {
|
||||
_animationController.forward();
|
||||
} else {
|
||||
_animationController.reverse();
|
||||
}
|
||||
});
|
||||
|
||||
_searchController.addListener(() {
|
||||
_filterSuggestions(_searchController.text);
|
||||
});
|
||||
}
|
||||
|
||||
void _filterSuggestions(String query) {
|
||||
if (query.isEmpty) {
|
||||
setState(() {
|
||||
_filteredSuggestions = widget.suggestions ?? _defaultSuggestions ?? [];
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
final defaultList = widget.suggestions ?? _defaultSuggestions ?? [];
|
||||
final filtered = defaultList
|
||||
.where((suggestion) =>
|
||||
suggestion.title.toLowerCase().contains(query.toLowerCase()) ||
|
||||
suggestion.subtitle.toLowerCase().contains(query.toLowerCase()))
|
||||
.toList();
|
||||
|
||||
setState(() {
|
||||
_filteredSuggestions = filtered;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (_defaultSuggestions == null) {
|
||||
_defaultSuggestions = _getDefaultSuggestions(context);
|
||||
if (_filteredSuggestions.isEmpty && widget.suggestions == null) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (mounted) setState(() => _filteredSuggestions = _defaultSuggestions!);
|
||||
});
|
||||
}
|
||||
}
|
||||
return Column(
|
||||
children: [
|
||||
_buildSearchBar(),
|
||||
if (_isExpanded && _filteredSuggestions.isNotEmpty) ...[
|
||||
const SizedBox(height: 8),
|
||||
_buildSuggestions(),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSearchBar() {
|
||||
return AnimatedBuilder(
|
||||
animation: _scaleAnimation,
|
||||
builder: (context, child) {
|
||||
return Transform.scale(
|
||||
scale: _scaleAnimation.value,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).cardColor,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: _isExpanded
|
||||
? [BoxShadow(color: Colors.black.withOpacity(0.1), blurRadius: 10, offset: const Offset(0, 4))]
|
||||
: [BoxShadow(color: Colors.black.withOpacity(0.05), blurRadius: 5, offset: const Offset(0, 2))],
|
||||
),
|
||||
child: TextField(
|
||||
controller: _searchController,
|
||||
focusNode: _focusNode,
|
||||
onSubmitted: (value) {
|
||||
if (value.isNotEmpty) {
|
||||
widget.onSearch?.call(value);
|
||||
_focusNode.unfocus();
|
||||
}
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
hintText: widget.hintText ?? 'Rechercher...',
|
||||
hintStyle: AppTypography.bodyTextSmall.copyWith(
|
||||
color: AppColors.textSecondaryLight,
|
||||
),
|
||||
prefixIcon: Icon(
|
||||
Icons.search_outlined,
|
||||
color: _isExpanded ? AppColors.primaryGreen : AppColors.textSecondaryLight,
|
||||
size: 20,
|
||||
),
|
||||
suffixIcon: _searchController.text.isNotEmpty
|
||||
? IconButton(
|
||||
onPressed: () {
|
||||
_searchController.clear();
|
||||
_focusNode.unfocus();
|
||||
},
|
||||
icon: const Icon(
|
||||
Icons.close_outlined,
|
||||
color: AppColors.textSecondaryLight,
|
||||
size: 18,
|
||||
),
|
||||
)
|
||||
: null,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: const BorderSide(
|
||||
color: AppColors.primaryGreen,
|
||||
width: 1.5,
|
||||
),
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 12,
|
||||
),
|
||||
filled: true,
|
||||
fillColor: Theme.of(context).cardColor,
|
||||
),
|
||||
style: AppTypography.bodyTextSmall,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSuggestions() {
|
||||
return Container(
|
||||
constraints: const BoxConstraints(maxHeight: 300),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).cardColor,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: ListView.builder(
|
||||
shrinkWrap: true,
|
||||
itemCount: _filteredSuggestions.length,
|
||||
itemBuilder: (context, index) {
|
||||
final suggestion = _filteredSuggestions[index];
|
||||
return _buildSuggestionItem(suggestion, index == _filteredSuggestions.length - 1);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSuggestionItem(SearchSuggestion suggestion, bool isLast) {
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
_searchController.text = suggestion.title;
|
||||
widget.onSearch?.call(suggestion.title);
|
||||
_focusNode.unfocus();
|
||||
suggestion.onTap?.call();
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
border: isLast
|
||||
? null
|
||||
: const Border(
|
||||
bottom: BorderSide(
|
||||
color: AppColors.lightBorder,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(6),
|
||||
decoration: BoxDecoration(
|
||||
color: suggestion.color.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: Icon(
|
||||
suggestion.icon,
|
||||
color: suggestion.color,
|
||||
size: 18,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
suggestion.title,
|
||||
style: AppTypography.actionText.copyWith(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
if (suggestion.subtitle.isNotEmpty) ...[
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
suggestion.subtitle,
|
||||
style: AppTypography.subtitleSmall.copyWith(
|
||||
color: AppColors.textSecondaryLight,
|
||||
fontSize: 10,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
const Icon(
|
||||
Icons.chevron_right_outlined,
|
||||
color: AppColors.textSecondaryLight,
|
||||
size: 16,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<SearchSuggestion> _getDefaultSuggestions(BuildContext context) {
|
||||
return [
|
||||
SearchSuggestion(
|
||||
title: 'Membres',
|
||||
subtitle: 'Rechercher des membres',
|
||||
icon: Icons.people_outline,
|
||||
color: AppColors.primaryGreen,
|
||||
onTap: () => Navigator.of(context).push(MaterialPageRoute<void>(builder: (_) => const MembersPageWrapper())),
|
||||
),
|
||||
SearchSuggestion(
|
||||
title: 'Événements',
|
||||
subtitle: 'Trouver des événements',
|
||||
icon: Icons.event_outlined,
|
||||
color: AppColors.brandGreen,
|
||||
onTap: () => Navigator.of(context).push(MaterialPageRoute<void>(builder: (_) => const EventsPageWrapper())),
|
||||
),
|
||||
SearchSuggestion(
|
||||
title: 'Contributions',
|
||||
subtitle: 'Historique des paiements',
|
||||
icon: Icons.account_balance_wallet_outlined,
|
||||
color: AppColors.success,
|
||||
onTap: () => Navigator.of(context).push(MaterialPageRoute<void>(builder: (_) => const ContributionsPageWrapper())),
|
||||
),
|
||||
SearchSuggestion(
|
||||
title: 'Rapports',
|
||||
subtitle: 'Consulter les rapports',
|
||||
icon: Icons.assessment_outlined,
|
||||
color: AppColors.warning,
|
||||
onTap: () => Navigator.of(context).push(MaterialPageRoute<void>(builder: (_) => const ReportsPageWrapper())),
|
||||
),
|
||||
SearchSuggestion(
|
||||
title: 'Paramètres',
|
||||
subtitle: 'Configuration système',
|
||||
icon: Icons.settings_outlined,
|
||||
color: AppColors.textSecondaryLight,
|
||||
onTap: () => Navigator.of(context).push(MaterialPageRoute<void>(builder: (_) => const SystemSettingsPage())),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_searchController.dispose();
|
||||
_focusNode.dispose();
|
||||
_animationController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
class SearchSuggestion {
|
||||
final String title;
|
||||
final String subtitle;
|
||||
final IconData icon;
|
||||
final Color color;
|
||||
final VoidCallback? onTap;
|
||||
|
||||
const SearchSuggestion({
|
||||
required this.title,
|
||||
required this.subtitle,
|
||||
required this.icon,
|
||||
required this.color,
|
||||
this.onTap,
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user