From 1fc58339b1ebf5533574e174b0464543b30b1057 Mon Sep 17 00:00:00 2001 From: ssbg04 Date: Sat, 20 Jun 2026 16:24:10 +0800 Subject: [PATCH] add: search bar sliding, implement: dashbord, students, documents, archive, users search bar --- backend/{.example.env => ..example.env} | 0 .../ui/screens/archives/archives_screen.dart | 15 ++- .../screens/dashboard/dashboard_screen.dart | 23 ++-- .../screens/documents/documents_screen.dart | 15 ++- .../ui/screens/students/students_screen.dart | 13 +- .../lib/ui/screens/users/users_screen.dart | 13 +- .../lib/ui/shared/inputs/app_search_bar.dart | 117 +++++++++++------- 7 files changed, 125 insertions(+), 71 deletions(-) rename backend/{.example.env => ..example.env} (100%) diff --git a/backend/.example.env b/backend/..example.env similarity index 100% rename from backend/.example.env rename to backend/..example.env diff --git a/frontend/lib/ui/screens/archives/archives_screen.dart b/frontend/lib/ui/screens/archives/archives_screen.dart index 9c06c29..dd34144 100644 --- a/frontend/lib/ui/screens/archives/archives_screen.dart +++ b/frontend/lib/ui/screens/archives/archives_screen.dart @@ -532,11 +532,16 @@ class _ArchivesScreenState extends ConsumerState Row( children: [ Expanded( - child: AppSearchBar( - hint: 'Search by name, LRN, file…', - controller: _searchController, - onChanged: _onSearchChanged, - maxWidth: double.infinity, + child: LayoutBuilder( + builder: (context, constraints) => Align( + alignment: Alignment.centerLeft, + child: AppSearchBar( + hint: 'Search by name, LRN, file…', + controller: _searchController, + onChanged: _onSearchChanged, + maxWidth: constraints.maxWidth, + ), + ), ), ), const SizedBox(width: 8), diff --git a/frontend/lib/ui/screens/dashboard/dashboard_screen.dart b/frontend/lib/ui/screens/dashboard/dashboard_screen.dart index d80bdf2..b994dde 100644 --- a/frontend/lib/ui/screens/dashboard/dashboard_screen.dart +++ b/frontend/lib/ui/screens/dashboard/dashboard_screen.dart @@ -505,15 +505,20 @@ class _DashboardScreenState extends ConsumerState { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( - child: AppSearchBar( - controller: _searchController, - hint: 'Search students by LRN or Name...', - maxWidth: 420, - onSubmitted: (value) { - ref.read(studentQueryProvider.notifier).setSearch(value); - ref.read(activeTabProvider.notifier).setTab('Students'); - _searchController.clear(); - }, + child: LayoutBuilder( + builder: (context, constraints) => Align( + alignment: Alignment.centerLeft, + child: AppSearchBar( + controller: _searchController, + hint: 'Search students by LRN or Name...', + maxWidth: constraints.maxWidth > 420 ? 420 : constraints.maxWidth, + onSubmitted: (value) { + ref.read(studentQueryProvider.notifier).setSearch(value); + ref.read(activeTabProvider.notifier).setTab('Students'); + _searchController.clear(); + }, + ), + ), ), ), const SizedBox(width: 16), diff --git a/frontend/lib/ui/screens/documents/documents_screen.dart b/frontend/lib/ui/screens/documents/documents_screen.dart index 865fd4f..b036f38 100644 --- a/frontend/lib/ui/screens/documents/documents_screen.dart +++ b/frontend/lib/ui/screens/documents/documents_screen.dart @@ -1028,11 +1028,16 @@ class _DocumentsScreenState extends ConsumerState children: [ // Custom search bar (expands to fill available width) Expanded( - child: AppSearchBar( - hint: 'Search by name, LRN, file…', - controller: _searchController, - onChanged: _onSearchChanged, - maxWidth: double.infinity, + child: LayoutBuilder( + builder: (context, constraints) => Align( + alignment: Alignment.centerLeft, + child: AppSearchBar( + hint: 'Search by name, LRN, file…', + controller: _searchController, + onChanged: _onSearchChanged, + maxWidth: constraints.maxWidth, + ), + ), ), ), const SizedBox(width: 8), diff --git a/frontend/lib/ui/screens/students/students_screen.dart b/frontend/lib/ui/screens/students/students_screen.dart index c95033f..8258104 100644 --- a/frontend/lib/ui/screens/students/students_screen.dart +++ b/frontend/lib/ui/screens/students/students_screen.dart @@ -359,10 +359,15 @@ class _StudentsScreenState extends ConsumerState { const SizedBox(height: AppSizes.p16), // Search bar (full width) - AppSearchBar( - controller: _searchController, - hint: 'Search by LRN or name...', - maxWidth: double.infinity, + LayoutBuilder( + builder: (context, constraints) => Align( + alignment: Alignment.centerLeft, + child: AppSearchBar( + controller: _searchController, + hint: 'Search by LRN or name...', + maxWidth: constraints.maxWidth, + ), + ), ), const SizedBox(height: AppSizes.p12), diff --git a/frontend/lib/ui/screens/users/users_screen.dart b/frontend/lib/ui/screens/users/users_screen.dart index c3acfdf..2544240 100644 --- a/frontend/lib/ui/screens/users/users_screen.dart +++ b/frontend/lib/ui/screens/users/users_screen.dart @@ -327,10 +327,15 @@ class _UsersScreenState extends ConsumerState { children: [ Expanded( flex: 2, - child: AppSearchBar( - hint: 'Search by username, name or role...', - controller: _searchController, - maxWidth: double.infinity, + child: LayoutBuilder( + builder: (context, constraints) => Align( + alignment: Alignment.centerLeft, + child: AppSearchBar( + hint: 'Search by username, name or role...', + controller: _searchController, + maxWidth: constraints.maxWidth, + ), + ), ), ), const SizedBox(width: AppSizes.p16), diff --git a/frontend/lib/ui/shared/inputs/app_search_bar.dart b/frontend/lib/ui/shared/inputs/app_search_bar.dart index ea7317f..f5a456b 100644 --- a/frontend/lib/ui/shared/inputs/app_search_bar.dart +++ b/frontend/lib/ui/shared/inputs/app_search_bar.dart @@ -45,13 +45,16 @@ class AppSearchBar extends StatefulWidget { class _AppSearchBarState extends State { late final TextEditingController _controller; + final FocusNode _focusNode = FocusNode(); bool _hasText = false; + bool _isExpanded = false; @override void initState() { super.initState(); _controller = widget.controller ?? TextEditingController(); _controller.addListener(_onTextChanged); + _focusNode.addListener(_onFocusChanged); } void _onTextChanged() { @@ -59,58 +62,84 @@ class _AppSearchBarState extends State { if (hasText != _hasText) setState(() => _hasText = hasText); } + void _onFocusChanged() { + if (!_focusNode.hasFocus && _controller.text.isEmpty) { + setState(() => _isExpanded = false); + } + } + @override void dispose() { if (widget.controller == null) _controller.dispose(); + _focusNode.dispose(); super.dispose(); } @override Widget build(BuildContext context) { - return ConstrainedBox( - constraints: BoxConstraints(maxWidth: widget.maxWidth), - child: TextField( - controller: _controller, - textInputAction: TextInputAction.search, - onSubmitted: (val) { - if (val.trim().isNotEmpty) { - widget.onSubmitted?.call(val.trim()); - } - }, - onChanged: widget.onChanged, - style: const TextStyle(fontSize: 14), - decoration: InputDecoration( - hintText: widget.hint, - hintStyle: TextStyle(fontSize: 13, color: Colors.grey.shade500), - prefixIcon: const Padding( - padding: EdgeInsets.symmetric(horizontal: 10), - child: Icon(Icons.search_rounded, size: 20, color: AppColors.primaryGreen), - ), - prefixIconConstraints: const BoxConstraints(minWidth: 40, minHeight: 40), - suffixIcon: widget.showClear && _hasText - ? IconButton( - icon: Icon(Icons.close_rounded, size: 18, color: Colors.grey.shade500), - splashRadius: 16, - onPressed: () { - _controller.clear(); - widget.onChanged?.call(''); - }, - ) - : null, - filled: true, - fillColor: Colors.white, - contentPadding: const EdgeInsets.symmetric(vertical: 10, horizontal: 12), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), - borderSide: BorderSide(color: Colors.grey.shade200), - ), - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), - borderSide: BorderSide(color: Colors.grey.shade200), - ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), - borderSide: const BorderSide(color: AppColors.primaryGreen, width: 1.5), + return AnimatedContainer( + duration: const Duration(milliseconds: 250), + curve: Curves.easeOutCubic, + width: _isExpanded ? widget.maxWidth : 42.0, + height: 42.0, + clipBehavior: Clip.antiAlias, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: _focusNode.hasFocus ? AppColors.primaryGreen : Colors.grey.shade200, + width: _focusNode.hasFocus ? 1.5 : 1.0, + ), + ), + child: OverflowBox( + alignment: Alignment.centerLeft, + maxWidth: widget.maxWidth, + minWidth: widget.maxWidth, + maxHeight: 42.0, + minHeight: 42.0, + child: TextField( + controller: _controller, + focusNode: _focusNode, + textInputAction: TextInputAction.search, + onSubmitted: (val) { + if (val.trim().isNotEmpty) { + widget.onSubmitted?.call(val.trim()); + } + }, + onChanged: widget.onChanged, + style: const TextStyle(fontSize: 14), + decoration: InputDecoration( + hintText: widget.hint, + hintStyle: TextStyle(fontSize: 13, color: Colors.grey.shade500), + prefixIcon: GestureDetector( + onTap: () { + if (!_isExpanded) { + setState(() => _isExpanded = true); + _focusNode.requestFocus(); + } + }, + child: Container( + color: Colors.transparent, + child: const Icon(Icons.search_rounded, size: 20, color: AppColors.primaryGreen), + ), + ), + prefixIconConstraints: const BoxConstraints(minWidth: 42, minHeight: 42), + suffixIcon: widget.showClear && _hasText + ? IconButton( + icon: Icon(Icons.close_rounded, size: 18, color: Colors.grey.shade500), + splashRadius: 16, + onPressed: () { + _controller.clear(); + widget.onChanged?.call(''); + }, + ) + : null, + filled: false, + isDense: true, + contentPadding: const EdgeInsets.only(top: 11, bottom: 11, right: 12), + border: InputBorder.none, + enabledBorder: InputBorder.none, + focusedBorder: InputBorder.none, ), ), ),