add: search bar sliding, implement: dashbord, students, documents, archive, users search bar

This commit is contained in:
ssbg04 committed 2026-06-20 16:24:10 +08:00
1 parent 4e5a9c7c74
commit 1fc58339b1
7 files changed
+125 -71

No files matched your search

@@ -532,11 +532,16 @@ class _ArchivesScreenState extends ConsumerState<ArchivesScreen>
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),
@@ -505,15 +505,20 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
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),
@@ -1028,11 +1028,16 @@ class _DocumentsScreenState extends ConsumerState<DocumentsScreen>
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),
@@ -359,10 +359,15 @@ class _StudentsScreenState extends ConsumerState<StudentsScreen> {
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),
@@ -327,10 +327,15 @@ class _UsersScreenState extends ConsumerState<UsersScreen> {
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),
@@ -45,13 +45,16 @@ class AppSearchBar extends StatefulWidget {
class _AppSearchBarState extends State<AppSearchBar> {
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<AppSearchBar> {
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,
),
),
),