diff --git a/lib/common/widgets/empty_search_result_widget.dart b/lib/common/widgets/empty_search_result_widget.dart new file mode 100644 index 00000000..493974d4 --- /dev/null +++ b/lib/common/widgets/empty_search_result_widget.dart @@ -0,0 +1,26 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class EmptySearchResultWidget extends StatelessWidget { + const EmptySearchResultWidget({ + this.message = 'No results found', + super.key, + }); + + final String message; + + @override + Widget build(BuildContext context) { + return Center( + child: Text( + message, + textAlign: TextAlign.center, + style: context.textTheme.bodySmall?.copyWith( + color: ColorsManager.lightGreyColor, + fontWeight: FontWeight.w400, + ), + ), + ); + } +} diff --git a/lib/common/widgets/sidebar_communities_list.dart b/lib/common/widgets/sidebar_communities_list.dart new file mode 100644 index 00000000..191aab25 --- /dev/null +++ b/lib/common/widgets/sidebar_communities_list.dart @@ -0,0 +1,53 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/spaces_management/all_spaces/model/community_model.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class SidebarCommunitiesList extends StatelessWidget { + const SidebarCommunitiesList({ + required this.communities, + required this.itemBuilder, + required this.scrollController, + required this.onScrollToEnd, + super.key, + }); + + final List communities; + final Widget Function(BuildContext context, int index) itemBuilder; + final ScrollController scrollController; + final void Function() onScrollToEnd; + + bool _onNotification(ScrollEndNotification notification) { + final hasReachedEnd = notification.metrics.extentAfter == 0; + if (hasReachedEnd) { + onScrollToEnd.call(); + return true; + } + + return false; + } + + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: SizedBox( + width: context.screenWidth * 0.5, + child: Scrollbar( + scrollbarOrientation: ScrollbarOrientation.left, + thumbVisibility: true, + controller: scrollController, + child: NotificationListener( + onNotification: _onNotification, + child: ListView.builder( + shrinkWrap: true, + padding: const EdgeInsetsDirectional.only(start: 16), + itemCount: communities.length, + controller: scrollController, + itemBuilder: itemBuilder, + ), + ), + ), + ), + ); + } +} diff --git a/lib/pages/space_tree/view/custom_expansion.dart b/lib/pages/space_tree/view/custom_expansion.dart index 515a8448..e4e7f423 100644 --- a/lib/pages/space_tree/view/custom_expansion.dart +++ b/lib/pages/space_tree/view/custom_expansion.dart @@ -8,19 +8,20 @@ class CustomExpansionTileSpaceTree extends StatelessWidget { final bool isSelected; final bool isSoldCheck; final bool isExpanded; - final Function? onExpansionChanged; - final Function? onItemSelected; + final void Function()? onExpansionChanged; + final void Function()? onItemSelected; - const CustomExpansionTileSpaceTree( - {super.key, - this.spaceId, - required this.title, - this.children, - this.isExpanded = false, - this.onExpansionChanged, - this.onItemSelected, - required this.isSelected, - this.isSoldCheck = false}); + const CustomExpansionTileSpaceTree({ + required this.isSelected, + required this.title, + this.spaceId, + this.children, + this.onExpansionChanged, + this.onItemSelected, + this.isExpanded = false, + this.isSoldCheck = false, + super.key, + }); @override Widget build(BuildContext context) { @@ -30,50 +31,30 @@ class CustomExpansionTileSpaceTree extends StatelessWidget { children: [ Checkbox( value: isSoldCheck ? null : isSelected, - onChanged: (bool? value) { - if (onItemSelected != null) { - onItemSelected!(); - } - }, + onChanged: (value) => onItemSelected ?? () {}, tristate: true, - side: WidgetStateBorderSide.resolveWith((states) { - return const BorderSide(color: ColorsManager.grayBorder); - }), + side: WidgetStateBorderSide.resolveWith( + (states) => const BorderSide(color: ColorsManager.grayBorder), + ), fillColor: WidgetStateProperty.resolveWith((states) { if (states.contains(WidgetState.selected)) { return ColorsManager.blue1; - } else { - return ColorsManager.checkBoxFillColor; } + + return ColorsManager.checkBoxFillColor; }), checkColor: ColorsManager.whiteColors, ), - if (children != null && children!.isNotEmpty) - InkWell( - onTap: () { - if (onExpansionChanged != null) { - onExpansionChanged!(); - } - }, - child: Icon( - isExpanded ? Icons.keyboard_arrow_down : Icons.keyboard_arrow_right, - color: ColorsManager.lightGrayColor, - size: 16.0, - ), - ), + _buildExpansionIcon(), Expanded( child: GestureDetector( - onTap: () { - if (onItemSelected != null) { - onItemSelected!(); - } - }, + onTap: onItemSelected, child: Text( _capitalizeFirstLetter(title), style: Theme.of(context).textTheme.bodySmall!.copyWith( color: isSelected - ? ColorsManager.blackColor // Change color to black when selected - : ColorsManager.lightGrayColor, // Gray when not selected + ? ColorsManager.blackColor + : ColorsManager.lightGrayColor, fontWeight: FontWeight.w400, ), ), @@ -92,6 +73,20 @@ class CustomExpansionTileSpaceTree extends StatelessWidget { ); } + Widget _buildExpansionIcon() { + return Visibility( + visible: children != null && children!.isNotEmpty, + child: InkWell( + onTap: onExpansionChanged, + child: Icon( + isExpanded ? Icons.keyboard_arrow_down : Icons.keyboard_arrow_right, + color: ColorsManager.lightGrayColor, + size: 16.0, + ), + ), + ); + } + String _capitalizeFirstLetter(String text) { if (text.isEmpty) return text; return text[0].toUpperCase() + text.substring(1); diff --git a/lib/pages/space_tree/view/space_tree_view.dart b/lib/pages/space_tree/view/space_tree_view.dart index 5b70da06..3d5d00bf 100644 --- a/lib/pages/space_tree/view/space_tree_view.dart +++ b/lib/pages/space_tree/view/space_tree_view.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_svg/svg.dart'; import 'package:syncrow_web/common/widgets/search_bar.dart'; +import 'package:syncrow_web/common/widgets/sidebar_communities_list.dart'; import 'package:syncrow_web/pages/space_tree/bloc/space_tree_bloc.dart'; import 'package:syncrow_web/pages/space_tree/bloc/space_tree_event.dart'; import 'package:syncrow_web/pages/space_tree/bloc/space_tree_state.dart'; @@ -23,7 +24,13 @@ class SpaceTreeView extends StatefulWidget { } class _SpaceTreeViewState extends State { - final ScrollController _scrollController = ScrollController(); + late final ScrollController _scrollController; + + @override + void initState() { + _scrollController = ScrollController(); + super.initState(); + } @override void dispose() { @@ -34,225 +41,161 @@ class _SpaceTreeViewState extends State { @override Widget build(BuildContext context) { return BlocBuilder(builder: (context, state) { - List list = - state.searchQuery.isNotEmpty ? state.filteredCommunity : state.communityList; + final communities = state.searchQuery.isNotEmpty + ? state.filteredCommunity + : state.communityList; return Container( height: MediaQuery.sizeOf(context).height, decoration: widget.isSide == true - ? subSectionContainerDecoration.copyWith(color: ColorsManager.whiteColors) + ? subSectionContainerDecoration.copyWith( + color: ColorsManager.whiteColors) : const BoxDecoration(color: ColorsManager.whiteColors), child: state is SpaceTreeLoadingState ? const Center(child: CircularProgressIndicator()) : Column( children: [ - widget.isSide == true - ? Container( - decoration: const BoxDecoration( - color: ColorsManager.circleRolesBackground, - borderRadius: BorderRadius.only( - topRight: Radius.circular(20), topLeft: Radius.circular(20)), - ), - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Row( - children: [ - Expanded( - child: Container( - decoration: BoxDecoration( - borderRadius: const BorderRadius.all(Radius.circular(20)), - border: Border.all(color: ColorsManager.grayBorder)), - child: TextFormField( - style: context.textTheme.bodyMedium - ?.copyWith(color: ColorsManager.blackColor), - onChanged: (value) { - context.read().add(SearchQueryEvent(value)); - }, - decoration: textBoxDecoration(radios: 20)!.copyWith( - fillColor: Colors.white, - suffixIcon: Padding( - padding: const EdgeInsets.only(right: 16), - child: SvgPicture.asset( - Assets.textFieldSearch, - width: 24, - height: 24, + if (widget.isSide == true) + Container( + decoration: const BoxDecoration( + color: ColorsManager.circleRolesBackground, + borderRadius: BorderRadius.only( + topRight: Radius.circular(20), + topLeft: Radius.circular(20), + ), + ), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + children: [ + Expanded( + child: Container( + decoration: BoxDecoration( + borderRadius: const BorderRadius.all( + Radius.circular(20), + ), + border: Border.all( + color: ColorsManager.grayBorder, + ), + ), + child: TextFormField( + style: context.textTheme.bodyMedium?.copyWith( + color: ColorsManager.blackColor, + ), + onChanged: (value) => + context.read().add( + SearchQueryEvent(value), ), - ), - hintStyle: context.textTheme.bodyMedium?.copyWith( - fontWeight: FontWeight.w400, - fontSize: 12, - color: ColorsManager.textGray), + decoration: + textBoxDecoration(radios: 20)?.copyWith( + fillColor: Colors.white, + suffixIcon: Padding( + padding: const EdgeInsets.only(right: 16), + child: SvgPicture.asset( + Assets.textFieldSearch, + width: 24, + height: 24, ), ), + hintStyle: + context.textTheme.bodyMedium?.copyWith( + fontWeight: FontWeight.w400, + fontSize: 12, + color: ColorsManager.textGray, + ), ), ), - ], + ), ), - ), - ) - : CustomSearchBar( - onSearchChanged: (query) { - context.read().add(SearchQueryEvent(query)); - }, + ], ), + ), + ) + else + CustomSearchBar( + onSearchChanged: (query) => context.read().add( + SearchQueryEvent(query), + ), + ), const SizedBox(height: 16), Expanded( child: state.isSearching ? const Center(child: CircularProgressIndicator()) - : ListView( - shrinkWrap: true, - scrollDirection: Axis.horizontal, - children: [ - Container( - width: MediaQuery.sizeOf(context).width * 0.5, - padding: const EdgeInsets.all(8.0), - child: list.isEmpty - ? Center( - child: Text( - 'No results found', - style: Theme.of(context).textTheme.bodySmall!.copyWith( - color: ColorsManager.lightGrayColor, - fontWeight: FontWeight.w400, - ), - ), - ) - : Scrollbar( - scrollbarOrientation: ScrollbarOrientation.left, - thumbVisibility: true, - controller: _scrollController, - child: NotificationListener( - onNotification: (notification) { - if (notification is ScrollEndNotification && - notification.metrics.extentAfter == 0) { - // If the user has reached the end of the list Load more data - context.read().add(PaginationEvent( - state.paginationModel, state.communityList)); - } - return false; - }, - child: Padding( - padding: const EdgeInsets.only(left: 16), - child: ListView.builder( - shrinkWrap: true, - itemCount: list.length, - controller: _scrollController, - itemBuilder: (context, index) { - return CustomExpansionTileSpaceTree( - title: list[index].name, - isSelected: state.selectedCommunities - .contains(list[index].uuid), - isSoldCheck: state.selectedCommunities - .contains(list[index].uuid), - onExpansionChanged: () { - context.read().add( - OnCommunityExpanded(list[index].uuid)); - }, - isExpanded: state.expandedCommunities - .contains(list[index].uuid), - onItemSelected: () { - context.read().add( - OnCommunitySelected(list[index].uuid, - list[index].spaces)); - widget.onSelect(); - }, - children: list[index].spaces.map((space) { - return CustomExpansionTileSpaceTree( - title: space.name, - isExpanded: state.expandedSpaces - .contains(space.uuid), - onItemSelected: () { - context.read().add( - OnSpaceSelected( - list[index], - space.uuid ?? '', - space.children)); - widget.onSelect(); - }, - onExpansionChanged: () { - context.read().add( - OnSpaceExpanded(list[index].uuid, - space.uuid ?? '')); - }, - isSelected: state.selectedSpaces - .contains(space.uuid) || - state.soldCheck.contains(space.uuid), - isSoldCheck: - state.soldCheck.contains(space.uuid), - children: _buildNestedSpaces( - context, state, space, list[index]), - ); - }).toList(), - ); - }), + : SidebarCommunitiesList( + onScrollToEnd: () => context.read().add( + PaginationEvent( + state.paginationModel, + state.communityList, + ), + ), + scrollController: _scrollController, + communities: communities, + itemBuilder: (context, index) { + return CustomExpansionTileSpaceTree( + title: communities[index].name, + isSelected: state.selectedCommunities + .contains(communities[index].uuid), + isSoldCheck: state.selectedCommunities + .contains(communities[index].uuid), + onExpansionChanged: () => + context.read().add( + OnCommunityExpanded( + communities[index].uuid, ), ), + isExpanded: state.expandedCommunities.contains( + communities[index].uuid, + ), + onItemSelected: () { + context.read().add( + OnCommunitySelected( + communities[index].uuid, + communities[index].spaces, + ), + ); + widget.onSelect(); + }, + children: communities[index].spaces.map( + (space) { + return CustomExpansionTileSpaceTree( + title: space.name, + isExpanded: + state.expandedSpaces.contains(space.uuid), + onItemSelected: () { + context.read().add( + OnSpaceSelected( + communities[index], + space.uuid ?? '', + space.children, + ), + ); + widget.onSelect(); + }, + onExpansionChanged: () => + context.read().add( + OnSpaceExpanded( + communities[index].uuid, + space.uuid ?? '', + ), + ), + isSelected: state.selectedSpaces + .contains(space.uuid) || + state.soldCheck.contains(space.uuid), + isSoldCheck: + state.soldCheck.contains(space.uuid), + children: _buildNestedSpaces( + context, + state, + space, + communities[index], ), - ), - ], + ); + }, + ).toList(), + ); + }, ), ), if (state.paginationIsLoading) const CircularProgressIndicator(), - // Expanded( - // child: Padding( - // padding: const EdgeInsets.all(8.0), - // child: list.isEmpty - // ? Center( - // child: Text( - // 'No results found', - // style: Theme.of(context).textTheme.bodySmall!.copyWith( - // color: ColorsManager.lightGrayColor, // Gray when not selected - // fontWeight: FontWeight.w400, - // ), - // ), - // ) - // : ListView( - // shrinkWrap: true, - // children: list - // .map( - // (community) => CustomExpansionTileSpaceTree( - // title: community.name, - // isSelected: - // state.selectedCommunities.contains(community.uuid), - // isSoldCheck: - // state.selectedCommunities.contains(community.uuid), - // onExpansionChanged: () { - // context - // .read() - // .add(OnCommunityExpanded(community.uuid)); - // }, - // isExpanded: - // state.expandedCommunities.contains(community.uuid), - // onItemSelected: () { - // context.read().add( - // OnCommunitySelected(community.uuid, community.spaces)); - - // onSelect(); - // }, - // children: community.spaces.map((space) { - // return CustomExpansionTileSpaceTree( - // title: space.name, - // isExpanded: state.expandedSpaces.contains(space.uuid), - // onItemSelected: () { - // context.read().add(OnSpaceSelected( - // community.uuid, space.uuid ?? '', space.children)); - // onSelect(); - // }, - // onExpansionChanged: () { - // context.read().add( - // OnSpaceExpanded(community.uuid, space.uuid ?? '')); - // }, - // isSelected: state.selectedSpaces.contains(space.uuid) || - // state.soldCheck.contains(space.uuid), - // isSoldCheck: state.soldCheck.contains(space.uuid), - // children: _buildNestedSpaces( - // context, state, space, community.uuid), - // ); - // }).toList(), - // ), - // ) - // .toList(), - // ), - // ), - // ), ], ), ); @@ -260,22 +203,28 @@ class _SpaceTreeViewState extends State { } List _buildNestedSpaces( - BuildContext context, SpaceTreeState state, SpaceModel space, CommunityModel community) { + BuildContext context, + SpaceTreeState state, + SpaceModel space, + CommunityModel community, + ) { return space.children.map((child) { return CustomExpansionTileSpaceTree( - isSelected: - state.selectedSpaces.contains(child.uuid) || state.soldCheck.contains(child.uuid), + isSelected: state.selectedSpaces.contains(child.uuid) || + state.soldCheck.contains(child.uuid), isSoldCheck: state.soldCheck.contains(child.uuid), title: child.name, isExpanded: state.expandedSpaces.contains(child.uuid), onItemSelected: () { - context - .read() - .add(OnSpaceSelected(community, child.uuid ?? '', child.children)); + context.read().add( + OnSpaceSelected(community, child.uuid ?? '', child.children), + ); widget.onSelect(); }, onExpansionChanged: () { - context.read().add(OnSpaceExpanded(community.uuid, child.uuid ?? '')); + context.read().add( + OnSpaceExpanded(community.uuid, child.uuid ?? ''), + ); }, children: _buildNestedSpaces(context, state, child, community), ); diff --git a/lib/pages/spaces_management/all_spaces/widgets/sidebar_widget.dart b/lib/pages/spaces_management/all_spaces/widgets/sidebar_widget.dart index 3eb1c001..198ebc51 100644 --- a/lib/pages/spaces_management/all_spaces/widgets/sidebar_widget.dart +++ b/lib/pages/spaces_management/all_spaces/widgets/sidebar_widget.dart @@ -1,6 +1,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/common/widgets/empty_search_result_widget.dart'; import 'package:syncrow_web/common/widgets/search_bar.dart'; +import 'package:syncrow_web/common/widgets/sidebar_communities_list.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/bloc/space_management_bloc.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/bloc/space_management_event.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/model/community_model.dart'; @@ -30,6 +32,8 @@ class SidebarWidget extends StatefulWidget { } class _SidebarWidgetState extends State { + late final ScrollController _scrollController; + String _searchQuery = ''; String? _selectedSpaceUuid; String? _selectedId; @@ -37,9 +41,16 @@ class _SidebarWidgetState extends State { @override void initState() { _selectedId = widget.selectedSpaceUuid; + _scrollController = ScrollController(); super.initState(); } + @override + void dispose() { + _scrollController.dispose(); + super.dispose(); + } + @override void didUpdateWidget(covariant SidebarWidget oldWidget) { if (widget.selectedSpaceUuid != oldWidget.selectedSpaceUuid) { @@ -86,12 +97,14 @@ class _SidebarWidgetState extends State { return isSpaceSelected || anySubSpaceIsSelected; } + static const _width = 300.0; + @override Widget build(BuildContext context) { final filteredCommunities = _filteredCommunities(); return Container( - width: 300, + width: _width, decoration: subSectionContainerDecoration, child: Column( mainAxisSize: MainAxisSize.min, @@ -103,10 +116,18 @@ class _SidebarWidgetState extends State { ), const SizedBox(height: 16), Expanded( - child: ListView( - children: filteredCommunities - .map((community) => _buildCommunityTile(context, community)) - .toList(), + child: Visibility( + visible: filteredCommunities.isNotEmpty, + replacement: const EmptySearchResultWidget(), + child: SidebarCommunitiesList( + scrollController: _scrollController, + onScrollToEnd: () {}, + communities: filteredCommunities, + itemBuilder: (context, index) => _buildCommunityTile( + context, + filteredCommunities[index], + ), + ), ), ), ], @@ -134,11 +155,12 @@ class _SidebarWidgetState extends State { }, onExpansionChanged: (title, expanded) {}, children: community.spaces - .where((space) { - final isDeleted = space.status != SpaceStatus.deleted; - final isParentDeleted = space.status != SpaceStatus.parentDeleted; - return (isDeleted || isParentDeleted); - }) + .where( + (space) => { + SpaceStatus.deleted, + SpaceStatus.parentDeleted, + }.contains(space.status), + ) .map((space) => _buildSpaceTile(space: space, community: community)) .toList(), );