diff --git a/lib/common/widgets/app_loading_indicator.dart b/lib/common/widgets/app_loading_indicator.dart new file mode 100644 index 00000000..bc811c56 --- /dev/null +++ b/lib/common/widgets/app_loading_indicator.dart @@ -0,0 +1,10 @@ +import 'package:flutter/material.dart'; + +class AppLoadingIndicator extends StatelessWidget { + const AppLoadingIndicator({super.key}); + + @override + Widget build(BuildContext context) { + return const Center(child: CircularProgressIndicator()); + } +} diff --git a/lib/pages/space_management_v2/main_module/shared/models/paginated_data_model.dart b/lib/pages/space_management_v2/main_module/shared/models/paginated_data_model.dart new file mode 100644 index 00000000..ac35975d --- /dev/null +++ b/lib/pages/space_management_v2/main_module/shared/models/paginated_data_model.dart @@ -0,0 +1,43 @@ +import 'package:equatable/equatable.dart'; + +class PaginatedDataModel extends Equatable { + const PaginatedDataModel({ + required this.data, + required this.page, + required this.size, + required this.hasNext, + required this.totalItems, + required this.totalPages, + }); + + final List data; + final int page; + final int size; + final bool hasNext; + final int totalItems; + final int totalPages; + + factory PaginatedDataModel.fromJson( + Map json, + List Function(List) fromJsonList, + ) { + return PaginatedDataModel( + data: fromJsonList(json['data'] as List), + page: json['page'] as int? ?? 1, + size: json['size'] as int? ?? 25, + hasNext: json['hasNext'] as bool? ?? false, + totalItems: json['totalItem'] as int? ?? 0, + totalPages: json['totalPage'] as int? ?? 0, + ); + } + + @override + List get props => [ + data, + page, + size, + hasNext, + totalItems, + totalPages, + ]; +} diff --git a/lib/pages/space_management_v2/main_module/views/space_management_page.dart b/lib/pages/space_management_v2/main_module/views/space_management_page.dart new file mode 100644 index 00000000..957be65a --- /dev/null +++ b/lib/pages/space_management_v2/main_module/views/space_management_page.dart @@ -0,0 +1,47 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/shared/navigate_home_grid_view.dart'; +import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/space_management_body.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/data/services/debounced_communities_service.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/data/services/remote_communities_service.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/params/load_communities_param.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart'; +import 'package:syncrow_web/services/api/http_service.dart'; +import 'package:syncrow_web/utils/theme/responsive_text_theme.dart'; +import 'package:syncrow_web/web_layout/web_scaffold.dart'; + +class SpaceManagementPage extends StatelessWidget { + const SpaceManagementPage({super.key}); + + @override + Widget build(BuildContext context) { + return MultiBlocProvider( + providers: [ + BlocProvider( + create: (context) => CommunitiesBloc( + communitiesService: DebouncedCommunitiesService( + RemoteCommunitiesService(HTTPService()), + ), + )..add(const LoadCommunities(LoadCommunitiesParam())), + ), + BlocProvider(create: (context) => CommunitiesTreeSelectionBloc()), + ], + child: WebScaffold( + appBarTitle: Text( + 'Space Management', + style: ResponsiveTextTheme.of(context).deviceManagementTitle, + ), + enableMenuSidebar: false, + centerBody: Text( + 'Community Structure', + style: Theme.of(context).textTheme.bodyLarge!.copyWith( + fontWeight: FontWeight.bold, + ), + ), + rightBody: const NavigateHomeGridView(), + scaffoldBody: const SpaceManagementBody(), + ), + ); + } +} diff --git a/lib/pages/space_management_v2/main_module/widgets/space_management_body.dart b/lib/pages/space_management_v2/main_module/widgets/space_management_body.dart new file mode 100644 index 00000000..3a9aa3c8 --- /dev/null +++ b/lib/pages/space_management_v2/main_module/widgets/space_management_body.dart @@ -0,0 +1,15 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree.dart'; + +class SpaceManagementBody extends StatelessWidget { + const SpaceManagementBody({super.key}); + + @override + Widget build(BuildContext context) { + return const Row( + children: [ + SpaceManagementCommunitiesTree(), + ], + ); + } +} diff --git a/lib/pages/space_management_v2/modules/communities/data/services/debounced_communities_service.dart b/lib/pages/space_management_v2/modules/communities/data/services/debounced_communities_service.dart new file mode 100644 index 00000000..a97e8524 --- /dev/null +++ b/lib/pages/space_management_v2/modules/communities/data/services/debounced_communities_service.dart @@ -0,0 +1,42 @@ +import 'dart:async'; + +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/params/load_communities_param.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/services/communities_service.dart'; + +final class DebouncedCommunitiesService implements CommunitiesService { + DebouncedCommunitiesService( + this._decoratee, { + this.debounceDuration = const Duration(milliseconds: 500), + }); + + final CommunitiesService _decoratee; + final Duration debounceDuration; + + Timer? _debounceTimer; + late Completer? _completer; + + @override + Future getCommunity( + LoadCommunitiesParam param, + ) async { + _debounceTimer?.cancel(); + + _completer = Completer(); + final currentCompleter = _completer!; + + _debounceTimer = Timer(debounceDuration, () async { + try { + final result = await _decoratee.getCommunity(param); + if (!currentCompleter.isCompleted) { + currentCompleter.complete(result); + } + } catch (error) { + if (!currentCompleter.isCompleted) { + currentCompleter.completeError(error); + } + } + }); + + return currentCompleter.future; + } +} diff --git a/lib/pages/space_management_v2/modules/communities/data/services/remote_communities_service.dart b/lib/pages/space_management_v2/modules/communities/data/services/remote_communities_service.dart index 36682bb4..cc842de8 100644 --- a/lib/pages/space_management_v2/modules/communities/data/services/remote_communities_service.dart +++ b/lib/pages/space_management_v2/modules/communities/data/services/remote_communities_service.dart @@ -1,9 +1,11 @@ import 'package:dio/dio.dart'; +import 'package:syncrow_web/pages/common/bloc/project_manager.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/params/load_communities_param.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/services/communities_service.dart'; import 'package:syncrow_web/services/api/api_exception.dart'; import 'package:syncrow_web/services/api/http_service.dart'; +import 'package:syncrow_web/utils/constants/api_const.dart'; class RemoteCommunitiesService implements CommunitiesService { const RemoteCommunitiesService(this._httpService); @@ -13,14 +15,26 @@ class RemoteCommunitiesService implements CommunitiesService { static const _defaultErrorMessage = 'Failed to load communities'; @override - Future> getCommunity(LoadCommunitiesParam param) async { + Future getCommunity( + LoadCommunitiesParam param, + ) async { try { - return _httpService.get( - path: '/api/communities/', - expectedResponseModel: (json) => (json as List) - .map((e) => CommunityModel.fromJson(e as Map)) - .toList(), + final response = await _httpService.get( + path: await _makeUrl(), + queryParameters: { + 'page': param.page, + 'size': param.size, + 'includeSpaces': param.includeSpaces, + if (param.search.isNotEmpty && param.search != 'null') + 'search': param.search, + }, + expectedResponseModel: (json) => CommunitiesPaginationModel.fromJson( + json as Map, + CommunityModel.fromJsonList, + ), ); + + return response; } on DioException catch (e) { final message = e.response?.data as Map?; final error = message?['error'] as Map?; @@ -31,4 +45,13 @@ class RemoteCommunitiesService implements CommunitiesService { throw APIException(formattedErrorMessage); } } + + Future _makeUrl() async { + final projectUuid = await ProjectManager.getProjectUUID(); + if (projectUuid == null) throw APIException('Project UUID is required'); + return ApiEndpoints.getCommunityListv2.replaceAll( + '{projectId}', + projectUuid, + ); + } } diff --git a/lib/pages/space_management_v2/modules/communities/domain/models/community_model.dart b/lib/pages/space_management_v2/modules/communities/domain/models/community_model.dart index c6efad9e..37f131b3 100644 --- a/lib/pages/space_management_v2/modules/communities/domain/models/community_model.dart +++ b/lib/pages/space_management_v2/modules/communities/domain/models/community_model.dart @@ -4,11 +4,19 @@ import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain class CommunityModel extends Equatable { final String uuid; final String name; + final DateTime createdAt; + final DateTime updatedAt; + final String description; + final String externalId; final List spaces; const CommunityModel({ required this.uuid, required this.name, + required this.createdAt, + required this.updatedAt, + required this.description, + required this.externalId, required this.spaces, }); @@ -16,11 +24,20 @@ class CommunityModel extends Equatable { return CommunityModel( uuid: json['uuid'] as String, name: json['name'] as String, - spaces: (json['spaces'] as List) + createdAt: DateTime.parse(json['createdAt'] as String), + updatedAt: DateTime.parse(json['updatedAt'] as String), + description: json['description'] as String, + externalId: json['externalId']?.toString() ?? '', + spaces: (json['spaces'] as List? ?? []) .map((e) => SpaceModel.fromJson(e as Map)) .toList(), ); } + static List fromJsonList(List json) { + return json + .map((e) => CommunityModel.fromJson(e as Map)) + .toList(); + } @override List get props => [uuid, name, spaces]; diff --git a/lib/pages/space_management_v2/modules/communities/domain/models/space_model.dart b/lib/pages/space_management_v2/modules/communities/domain/models/space_model.dart index 0f8aadb2..36943adb 100644 --- a/lib/pages/space_management_v2/modules/communities/domain/models/space_model.dart +++ b/lib/pages/space_management_v2/modules/communities/domain/models/space_model.dart @@ -2,26 +2,37 @@ import 'package:equatable/equatable.dart'; class SpaceModel extends Equatable { final String uuid; + final DateTime? createdAt; + final DateTime? updatedAt; final String spaceName; final String icon; final List children; + final SpaceModel? parent; const SpaceModel({ required this.uuid, + required this.createdAt, + required this.updatedAt, required this.spaceName, required this.icon, required this.children, + required this.parent, }); factory SpaceModel.fromJson(Map json) { return SpaceModel( - uuid: json['uuid'] as String, - spaceName: json['spaceName'] as String, - icon: json['icon'] as String, + uuid: json['uuid'] as String? ?? '', + createdAt: DateTime.tryParse(json['createdAt'] as String? ?? ''), + updatedAt: DateTime.tryParse(json['updatedAt'] as String? ?? ''), + spaceName: json['spaceName'] as String? ?? '', + icon: json['icon'] as String? ?? 'assets/icons/location_icon.svg', children: (json['children'] as List?) ?.map((e) => SpaceModel.fromJson(e as Map)) .toList() ?? [], + parent: json['parent'] != null + ? SpaceModel.fromJson(json['parent'] as Map) + : null, ); } diff --git a/lib/pages/space_management_v2/modules/communities/domain/params/load_communities_param.dart b/lib/pages/space_management_v2/modules/communities/domain/params/load_communities_param.dart index 9bdc215c..774c4c31 100644 --- a/lib/pages/space_management_v2/modules/communities/domain/params/load_communities_param.dart +++ b/lib/pages/space_management_v2/modules/communities/domain/params/load_communities_param.dart @@ -1,3 +1,32 @@ -class LoadCommunitiesParam { - const LoadCommunitiesParam(); +import 'package:equatable/equatable.dart'; + +class LoadCommunitiesParam extends Equatable { + const LoadCommunitiesParam({ + this.page = 1, + this.size = 25, + this.search = '', + this.includeSpaces = true, + }); + + final int page; + final int size; + final String search; + final bool includeSpaces; + + LoadCommunitiesParam copyWith({ + int? page, + int? size, + String? search, + bool? includeSpaces, + }) { + return LoadCommunitiesParam( + page: page ?? this.page, + size: size ?? this.size, + search: search ?? this.search, + includeSpaces: includeSpaces ?? this.includeSpaces, + ); + } + + @override + List get props => [page, size, search, includeSpaces]; } diff --git a/lib/pages/space_management_v2/modules/communities/domain/services/communities_service.dart b/lib/pages/space_management_v2/modules/communities/domain/services/communities_service.dart index bccad2ad..baa84590 100644 --- a/lib/pages/space_management_v2/modules/communities/domain/services/communities_service.dart +++ b/lib/pages/space_management_v2/modules/communities/domain/services/communities_service.dart @@ -1,6 +1,9 @@ +import 'package:syncrow_web/pages/space_management_v2/main_module/shared/models/paginated_data_model.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/params/load_communities_param.dart'; +typedef CommunitiesPaginationModel = PaginatedDataModel; + abstract class CommunitiesService { - Future> getCommunity(LoadCommunitiesParam param); + Future getCommunity(LoadCommunitiesParam param); } diff --git a/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart b/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart index 0d85b22f..9094a632 100644 --- a/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart +++ b/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart @@ -14,6 +14,8 @@ class CommunitiesBloc extends Bloc { }) : _communitiesService = communitiesService, super(const CommunitiesState()) { on(_onLoadCommunities); + on(_onLoadMoreCommunities); + on(_onInsertCommunity); } final CommunitiesService _communitiesService; @@ -23,28 +25,93 @@ class CommunitiesBloc extends Bloc { Emitter emit, ) async { try { - emit(const CommunitiesState(status: CommunitiesStatus.loading)); - final communities = await _communitiesService.getCommunity(event.param); + emit( + state.copyWith(status: CommunitiesStatus.loading), + ); + + final paginationResponse = await _communitiesService.getCommunity( + event.param, + ); + emit( CommunitiesState( status: CommunitiesStatus.success, - communities: communities, + communities: paginationResponse.data, + hasNext: paginationResponse.hasNext, + currentPage: paginationResponse.page, + searchQuery: event.param.search, + isLoadingMore: false, ), ); } on APIException catch (e) { - emit( - CommunitiesState( - status: CommunitiesStatus.failure, - errorMessage: e.message, - ), - ); + _onApiException(e, emit); } catch (e) { - emit( - CommunitiesState( - status: CommunitiesStatus.failure, - errorMessage: e.toString(), - ), - ); + _onError(e, emit); } } + + Future _onLoadMoreCommunities( + LoadMoreCommunities event, + Emitter emit, + ) async { + if (!state.hasNext || state.isLoadingMore) return; + + try { + emit(state.copyWith(isLoadingMore: true)); + + final param = LoadCommunitiesParam( + page: state.currentPage + 1, + search: state.searchQuery, + ); + + final paginationResponse = await _communitiesService.getCommunity(param); + + final updatedCommunities = List.from(state.communities) + ..addAll(paginationResponse.data); + + emit( + state.copyWith( + status: CommunitiesStatus.success, + communities: updatedCommunities, + hasNext: paginationResponse.hasNext, + currentPage: paginationResponse.page, + isLoadingMore: false, + ), + ); + } on APIException catch (e) { + _onApiException(e, emit); + } catch (e) { + _onError(e, emit); + } + } + + void _onApiException( + APIException e, + Emitter emit, + ) { + emit( + state.copyWith( + status: CommunitiesStatus.failure, + isLoadingMore: false, + errorMessage: e.message, + ), + ); + } + + void _onError(Object e, Emitter emit) { + emit( + state.copyWith( + status: CommunitiesStatus.failure, + isLoadingMore: false, + errorMessage: e.toString(), + ), + ); + } + + void _onInsertCommunity( + InsertCommunity event, + Emitter emit, + ) { + emit(state.copyWith(communities: [event.community, ...state.communities])); + } } diff --git a/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_event.dart b/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_event.dart index ef375c5a..cd14fa3d 100644 --- a/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_event.dart +++ b/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_event.dart @@ -15,3 +15,19 @@ class LoadCommunities extends CommunitiesEvent { @override List get props => [param]; } + +class LoadMoreCommunities extends CommunitiesEvent { + const LoadMoreCommunities(); + + @override + List get props => []; +} + +final class InsertCommunity extends CommunitiesEvent { + const InsertCommunity(this.community); + + final CommunityModel community; + + @override + List get props => [community]; +} diff --git a/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_state.dart b/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_state.dart index 94740f0b..c0e57ffd 100644 --- a/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_state.dart +++ b/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_state.dart @@ -7,12 +7,48 @@ final class CommunitiesState extends Equatable { this.status = CommunitiesStatus.initial, this.communities = const [], this.errorMessage, + this.isLoadingMore = false, + this.hasNext = false, + this.currentPage = 1, + this.searchQuery = '', }); final CommunitiesStatus status; final List communities; final String? errorMessage; + final bool isLoadingMore; + final bool hasNext; + final int currentPage; + final String searchQuery; + + CommunitiesState copyWith({ + CommunitiesStatus? status, + List? communities, + String? errorMessage, + bool? isLoadingMore, + bool? hasNext, + int? currentPage, + String? searchQuery, + }) { + return CommunitiesState( + status: status ?? this.status, + communities: communities ?? this.communities, + errorMessage: errorMessage ?? this.errorMessage, + isLoadingMore: isLoadingMore ?? this.isLoadingMore, + hasNext: hasNext ?? this.hasNext, + currentPage: currentPage ?? this.currentPage, + searchQuery: searchQuery ?? this.searchQuery, + ); + } @override - List get props => [status, communities, errorMessage]; + List get props => [ + status, + communities, + errorMessage, + isLoadingMore, + hasNext, + currentPage, + searchQuery, + ]; } diff --git a/lib/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart b/lib/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart new file mode 100644 index 00000000..bfc02f11 --- /dev/null +++ b/lib/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart @@ -0,0 +1,47 @@ +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/space_model.dart'; + +part 'communities_tree_selection_event.dart'; +part 'communities_tree_selection_state.dart'; + +class CommunitiesTreeSelectionBloc + extends Bloc { + CommunitiesTreeSelectionBloc() : super(const CommunitiesTreeSelectionState()) { + on(_onSelectCommunity); + on(_onSelectSpace); + on(_onClearSelection); + } + + void _onSelectCommunity( + SelectCommunityEvent event, + Emitter emit, + ) { + emit( + CommunitiesTreeSelectionState( + selectedCommunity: event.community, + selectedSpace: null, + ), + ); + } + + void _onSelectSpace( + SelectSpaceEvent event, + Emitter emit, + ) { + emit( + CommunitiesTreeSelectionState( + selectedCommunity: null, + selectedSpace: event.space, + ), + ); + } + + void _onClearSelection( + ClearCommunitiesTreeSelectionEvent event, + Emitter emit, + ) { + emit(const CommunitiesTreeSelectionState()); + } +} diff --git a/lib/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_event.dart b/lib/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_event.dart new file mode 100644 index 00000000..95ffe173 --- /dev/null +++ b/lib/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_event.dart @@ -0,0 +1,30 @@ +part of 'communities_tree_selection_bloc.dart'; + +sealed class CommunitiesTreeSelectionEvent extends Equatable { + const CommunitiesTreeSelectionEvent(); + + @override + List get props => []; +} + +final class SelectCommunityEvent extends CommunitiesTreeSelectionEvent { + final CommunityModel? community; + + const SelectCommunityEvent({required this.community}); + @override + List get props => [community]; +} + +final class SelectSpaceEvent extends CommunitiesTreeSelectionEvent { + final SpaceModel? space; + + const SelectSpaceEvent({required this.space}); + + @override + List get props => [space]; +} + +final class ClearCommunitiesTreeSelectionEvent + extends CommunitiesTreeSelectionEvent { + const ClearCommunitiesTreeSelectionEvent(); +} diff --git a/lib/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_state.dart b/lib/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_state.dart new file mode 100644 index 00000000..b14d330b --- /dev/null +++ b/lib/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_state.dart @@ -0,0 +1,29 @@ +part of 'communities_tree_selection_bloc.dart'; + +final class CommunitiesTreeSelectionState extends Equatable { + const CommunitiesTreeSelectionState({ + this.selectedCommunity, + this.selectedSpace, + }); + + final CommunityModel? selectedCommunity; + final SpaceModel? selectedSpace; + + CommunitiesTreeSelectionState copyWith({ + CommunityModel? selectedCommunity, + SpaceModel? selectedSpace, + List? expandedCommunities, + List? expandedSpaces, + }) { + return CommunitiesTreeSelectionState( + selectedCommunity: selectedCommunity ?? this.selectedCommunity, + selectedSpace: selectedSpace ?? this.selectedSpace, + ); + } + + @override + List get props => [ + selectedCommunity, + selectedSpace, + ]; + } diff --git a/lib/pages/space_management_v2/modules/communities/presentation/widgets/communities_tree_failure_widget.dart b/lib/pages/space_management_v2/modules/communities/presentation/widgets/communities_tree_failure_widget.dart new file mode 100644 index 00000000..cfd32f52 --- /dev/null +++ b/lib/pages/space_management_v2/modules/communities/presentation/widgets/communities_tree_failure_widget.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/params/load_communities_param.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart'; + +class CommunitiesTreeFailureWidget extends StatelessWidget { + const CommunitiesTreeFailureWidget({super.key, this.errorMessage}); + + final String? errorMessage; + + @override + Widget build(BuildContext context) { + return Expanded( + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + errorMessage ?? 'Something went wrong', + textAlign: TextAlign.center, + ), + const SizedBox(height: 16), + ElevatedButton( + onPressed: () => context.read().add( + LoadCommunities( + LoadCommunitiesParam( + search: context.read().state.searchQuery, + ), + ), + ), + child: const Text('Retry'), + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/space_management_v2/modules/communities/presentation/widgets/community_tile.dart b/lib/pages/space_management_v2/modules/communities/presentation/widgets/community_tile.dart new file mode 100644 index 00000000..0baaae52 --- /dev/null +++ b/lib/pages/space_management_v2/modules/communities/presentation/widgets/community_tile.dart @@ -0,0 +1,37 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/common/widgets/custom_expansion_tile.dart'; + +class CommunityTile extends StatelessWidget { + final String title; + final List? children; + final bool isExpanded; + final bool isSelected; + final void Function(String, bool isExpanded) onExpansionChanged; + final void Function() onItemSelected; + + const CommunityTile({ + super.key, + required this.title, + required this.isExpanded, + required this.onExpansionChanged, + required this.onItemSelected, + required this.isSelected, + this.children, + }); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(8.0), + child: CustomExpansionTile( + title: title, + initiallyExpanded: isExpanded, + isSelected: isSelected, + onExpansionChanged: (bool expanded) { + onExpansionChanged(title, expanded); + }, + onItemSelected: onItemSelected, + children: children ?? [], + )); + } +} diff --git a/lib/pages/space_management_v2/modules/communities/presentation/widgets/empty_communities_tree_search_result_widget.dart b/lib/pages/space_management_v2/modules/communities/presentation/widgets/empty_communities_tree_search_result_widget.dart new file mode 100644 index 00000000..bfc9e30e --- /dev/null +++ b/lib/pages/space_management_v2/modules/communities/presentation/widgets/empty_communities_tree_search_result_widget.dart @@ -0,0 +1,22 @@ +import 'package:flutter/material.dart'; + +class EmptyCommunitiesTreeSearchResultWidget extends StatelessWidget { + const EmptyCommunitiesTreeSearchResultWidget({ + required this.searchQuery, + super.key, + }); + + final String searchQuery; + + @override + Widget build(BuildContext context) { + return Center( + child: Text( + searchQuery.isEmpty + ? 'No communities found' + : 'No communities found for "$searchQuery"', + textAlign: TextAlign.center, + ), + ); + } +} diff --git a/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree.dart b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree.dart new file mode 100644 index 00000000..1adf9911 --- /dev/null +++ b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree.dart @@ -0,0 +1,112 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/common/widgets/app_loading_indicator.dart'; +import 'package:syncrow_web/common/widgets/search_bar.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/params/load_communities_param.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/communities_tree_failure_widget.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/empty_communities_tree_search_result_widget.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree_community_tile.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_communities_list.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_header.dart'; +import 'package:syncrow_web/utils/style.dart'; + +class SpaceManagementCommunitiesTree extends StatefulWidget { + const SpaceManagementCommunitiesTree({super.key}); + + @override + State createState() => + _SpaceManagementCommunitiesTreeState(); +} + +class _SpaceManagementCommunitiesTreeState + extends State { + @override + void initState() { + context.read().add( + const LoadCommunities(LoadCommunitiesParam()), + ); + super.initState(); + } + + void _onSearchChanged(String searchQuery) { + context + .read() + .add(LoadCommunities(LoadCommunitiesParam(search: searchQuery.trim()))); + } + + void _onLoadMore() { + context.read().add(const LoadMoreCommunities()); + } + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) => Container( + width: 320, + decoration: subSectionContainerDecoration, + child: Column( + children: [ + const SpaceManagementSidebarHeader(), + CustomSearchBar( + onSearchChanged: _onSearchChanged, + ), + const SizedBox(height: 16), + switch (state.status) { + CommunitiesStatus.initial => const AppLoadingIndicator(), + CommunitiesStatus.loading => state.communities.isEmpty + ? const AppLoadingIndicator() + : _buildCommunitiesTree(context, state), + CommunitiesStatus.success => _buildCommunitiesTree(context, state), + CommunitiesStatus.failure => CommunitiesTreeFailureWidget( + errorMessage: state.errorMessage, + ), + }, + Visibility( + visible: state.isLoadingMore, + child: const AppLoadingIndicator(), + ), + ], + ), + ), + ); + } + + Widget _buildCommunitiesTree( + BuildContext context, + CommunitiesState state, + ) { + final communitiesIsEmpty = state.communities.isEmpty; + final statusIsSuccess = state.status == CommunitiesStatus.success; + + return Expanded( + child: Visibility( + visible: statusIsSuccess && communitiesIsEmpty, + replacement: Stack( + children: [ + SpaceManagementSidebarCommunitiesList( + communities: state.communities, + onLoadMore: state.hasNext ? _onLoadMore : null, + isLoadingMore: state.isLoadingMore, + hasNext: state.hasNext, + itemBuilder: (context, index) { + return SpaceManagementCommunitiesTreeCommunityTile( + community: state.communities[index], + ); + }, + ), + if (state.status == CommunitiesStatus.loading && + state.communities.isNotEmpty) + ColoredBox( + color: Colors.white.withValues(alpha: 0.7), + child: const AppLoadingIndicator(), + ), + ], + ), + child: EmptyCommunitiesTreeSearchResultWidget( + searchQuery: state.searchQuery, + ), + ), + ); + } +} diff --git a/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree_community_tile.dart b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree_community_tile.dart new file mode 100644 index 00000000..736a499f --- /dev/null +++ b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree_community_tile.dart @@ -0,0 +1,45 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/community_tile.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree_space_tile.dart'; + +class SpaceManagementCommunitiesTreeCommunityTile extends StatelessWidget { + const SpaceManagementCommunitiesTreeCommunityTile({ + required this.community, + super.key, + }); + + final CommunityModel community; + + @override + Widget build(BuildContext context) { + final spaces = community.spaces + .map( + (space) => SpaceManagementCommunitiesTreeSpaceTile( + space: space, + community: community, + ), + ) + .toList(); + return CommunityTile( + title: community.name, + key: ValueKey(community.uuid), + isSelected: context + .watch() + .state + .selectedCommunity + ?.uuid == + community.uuid, + isExpanded: false, + onItemSelected: () { + context.read().add( + SelectCommunityEvent(community: community), + ); + }, + onExpansionChanged: (title, expanded) {}, + children: spaces, + ); + } +} diff --git a/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree_space_tile.dart b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree_space_tile.dart new file mode 100644 index 00000000..dcd44ac8 --- /dev/null +++ b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree_space_tile.dart @@ -0,0 +1,56 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/space_model.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/space_tile.dart'; + +class SpaceManagementCommunitiesTreeSpaceTile extends StatelessWidget { + const SpaceManagementCommunitiesTreeSpaceTile({ + required this.space, + required this.community, + super.key, + }); + + final SpaceModel space; + final CommunityModel community; + + @override + Widget build(BuildContext context) { + final spaceIsExpanded = _isSpaceOrChildSelected(context, space); + final isSelected = + context.watch().state.selectedSpace?.uuid == + space.uuid; + return Padding( + padding: const EdgeInsetsDirectional.only(start: 16.0), + child: SpaceTile( + title: space.spaceName, + key: ValueKey(space.uuid), + isSelected: isSelected, + initiallyExpanded: spaceIsExpanded, + onExpansionChanged: (expanded) {}, + onItemSelected: () => context.read().add( + SelectSpaceEvent(space: space), + ), + children: space.children + .map( + (childSpace) => SpaceManagementCommunitiesTreeSpaceTile( + space: childSpace, + community: community, + ), + ) + .toList(), + ), + ); + } + + bool _isSpaceOrChildSelected(BuildContext context, SpaceModel space) { + final selectedSpace = + context.read().state.selectedSpace; + final isSpaceSelected = selectedSpace?.uuid == space.uuid; + final anySubSpaceIsSelected = space.children.any( + (child) => _isSpaceOrChildSelected(context, child), + ); + return isSpaceSelected || anySubSpaceIsSelected; + } +} diff --git a/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_add_community_button.dart b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_add_community_button.dart new file mode 100644 index 00000000..ba281335 --- /dev/null +++ b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_add_community_button.dart @@ -0,0 +1,34 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; + +class SpaceManagementSidebarAddCommunityButton extends StatelessWidget { + const SpaceManagementSidebarAddCommunityButton({ + required this.onTap, + super.key, + }); + + final void Function() onTap; + + @override + Widget build(BuildContext context) { + return SizedBox.square( + dimension: 30, + child: IconButton( + style: IconButton.styleFrom( + iconSize: 20, + backgroundColor: ColorsManager.circleImageBackground, + shape: const CircleBorder( + side: BorderSide( + color: ColorsManager.lightGrayBorderColor, + width: 3, + ), + ), + ), + onPressed: onTap, + icon: SvgPicture.asset(Assets.addIcon), + ), + ); + } +} diff --git a/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_communities_list.dart b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_communities_list.dart new file mode 100644 index 00000000..40766be5 --- /dev/null +++ b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_communities_list.dart @@ -0,0 +1,104 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class SpaceManagementSidebarCommunitiesList extends StatefulWidget { + const SpaceManagementSidebarCommunitiesList({ + required this.communities, + required this.itemBuilder, + this.onLoadMore, + this.isLoadingMore = false, + this.hasNext = false, + super.key, + }); + + final List communities; + final Widget Function(BuildContext context, int index) itemBuilder; + final VoidCallback? onLoadMore; + final bool isLoadingMore; + final bool hasNext; + + @override + State createState() => + _SpaceManagementSidebarCommunitiesListState(); +} + +class _SpaceManagementSidebarCommunitiesListState + extends State { + late final ScrollController _scrollController; + + @override + void initState() { + super.initState(); + _scrollController = ScrollController(); + _scrollController.addListener(_onScroll); + } + + void _onScroll() { + if (_scrollController.position.pixels >= + _scrollController.position.maxScrollExtent - 100) { + if (widget.hasNext && !widget.isLoadingMore && widget.onLoadMore != null) { + widget.onLoadMore!(); + } + } + } + + bool _onNotification(ScrollEndNotification notification) { + final hasReachedEnd = notification.metrics.extentAfter == 0; + if (hasReachedEnd && + widget.hasNext && + !widget.isLoadingMore && + widget.onLoadMore != null) { + widget.onLoadMore!(); + return true; + } + + return false; + } + + @override + void dispose() { + _scrollController + ..removeListener(_onScroll) + ..dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final itemCount = widget.communities.length + (widget.isLoadingMore ? 1 : 0); + + 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: itemCount, + controller: _scrollController, + itemBuilder: (context, index) { + if (index == widget.communities.length) { + return const Padding( + padding: EdgeInsets.all(16.0), + child: Center( + child: CircularProgressIndicator(), + ), + ); + } + + return widget.itemBuilder(context, index); + }, + ), + ), + ), + ), + ); + } +} diff --git a/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_header.dart b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_header.dart new file mode 100644 index 00000000..25c094db --- /dev/null +++ b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_header.dart @@ -0,0 +1,68 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_add_community_button.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/create_community/presentation/create_community_dialog.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; +import 'package:syncrow_web/utils/style.dart'; + +class SpaceManagementSidebarHeader extends StatelessWidget { + const SpaceManagementSidebarHeader({super.key}); + + @override + Widget build(BuildContext context) { + return Container( + decoration: subSectionContainerDecoration, + padding: const EdgeInsets.all(16), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Communities', + style: context.textTheme.titleMedium?.copyWith( + color: ColorsManager.blackColor, + ), + ), + SpaceManagementSidebarAddCommunityButton( + onTap: () => _onAddCommunity(context), + ), + ], + ), + ); + } + + void _onAddCommunity(BuildContext context) { + final bloc = context.read(); + final selectedCommunity = bloc.state.selectedCommunity; + final isSelected = selectedCommunity?.uuid.isNotEmpty ?? false; + + if (isSelected) { + _clearSelection(context); + } else { + _showCreateCommunityDialog(context); + } + } + + void _clearSelection(BuildContext context) { + context.read().add( + const ClearCommunitiesTreeSelectionEvent(), + ); + } + + void _showCreateCommunityDialog(BuildContext context) => showDialog( + context: context, + builder: (_) => CreateCommunityDialog( + title: const Text('Community Name'), + onCreateCommunity: (community) { + context.read().add( + InsertCommunity(community), + ); + context.read().add( + SelectCommunityEvent(community: community), + ); + }, + ), + ); +} diff --git a/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_tile.dart b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_tile.dart new file mode 100644 index 00000000..d05199f0 --- /dev/null +++ b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_tile.dart @@ -0,0 +1,54 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/common/widgets/custom_expansion_tile.dart'; + +class SpaceTile extends StatefulWidget { + final String title; + final bool isSelected; + final bool initiallyExpanded; + final ValueChanged onExpansionChanged; + final List? children; + final void Function() onItemSelected; + + const SpaceTile({ + super.key, + required this.title, + required this.initiallyExpanded, + required this.onExpansionChanged, + required this.onItemSelected, + required this.isSelected, + this.children, + }); + + @override + State createState() => _SpaceTileState(); +} + +class _SpaceTileState extends State { + late bool _isExpanded; + + @override + void initState() { + super.initState(); + _isExpanded = widget.initiallyExpanded; + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(left: 8.0, right: 8.0, top: 8.0), + child: CustomExpansionTile( + isSelected: widget.isSelected, + title: widget.title, + initiallyExpanded: _isExpanded, + onItemSelected: widget.onItemSelected, + onExpansionChanged: (bool expanded) { + setState(() { + _isExpanded = expanded; + }); + widget.onExpansionChanged(expanded); + }, + children: widget.children ?? [], + ), + ); + } +} diff --git a/lib/pages/space_management_v2/modules/create_community/data/services/remote_create_community_service.dart b/lib/pages/space_management_v2/modules/create_community/data/services/remote_create_community_service.dart index be83124b..bd91f6ce 100644 --- a/lib/pages/space_management_v2/modules/create_community/data/services/remote_create_community_service.dart +++ b/lib/pages/space_management_v2/modules/create_community/data/services/remote_create_community_service.dart @@ -1,4 +1,5 @@ import 'package:dio/dio.dart'; +import 'package:syncrow_web/pages/common/bloc/project_manager.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/create_community/domain/param/create_community_param.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/create_community/domain/services/create_community_service.dart'; @@ -16,24 +17,51 @@ class RemoteCreateCommunityService implements CreateCommunityService { Future createCommunity(CreateCommunityParam param) async { try { final response = await _httpService.post( - path: 'endpoint', - expectedResponseModel: (data) => CommunityModel.fromJson( - data as Map, - ), + path: await _makeUrl(), + body: { + 'name': param.name, + 'description': param.description, + }, + expectedResponseModel: (data) { + final json = data as Map; + if (json['success'] == true) { + return CommunityModel.fromJson( + json['data'] as Map, + ); + } + return null; + }, ); + + if (response == null) { + throw APIException( + _getErrorMessageFromBody(response as Map?), + ); + } return response; } on DioException catch (e) { final message = e.response?.data as Map?; - final error = message?['error'] as Map?; - final errorMessage = error?['error'] as String? ?? ''; - final formattedErrorMessage = [ - _defaultErrorMessage, - errorMessage, - ].join(': '); - throw APIException(formattedErrorMessage); + throw APIException(_getErrorMessageFromBody(message)); } catch (e) { final formattedErrorMessage = [_defaultErrorMessage, '$e'].join(': '); throw APIException(formattedErrorMessage); } } + + String _getErrorMessageFromBody(Map? body) { + if (body == null) { + return _defaultErrorMessage; + } + final error = body['error'] as Map?; + final errorMessage = error?['error'] as String? ?? ''; + return errorMessage; + } + + Future _makeUrl() async { + final projectUuid = await ProjectManager.getProjectUUID(); + if (projectUuid == null) { + throw APIException('Project UUID is not set'); + } + return '/projects/$projectUuid/communities'; + } } diff --git a/lib/pages/space_management_v2/modules/create_community/domain/param/create_community_param.dart b/lib/pages/space_management_v2/modules/create_community/domain/param/create_community_param.dart index 3d7c203b..68a9fa11 100644 --- a/lib/pages/space_management_v2/modules/create_community/domain/param/create_community_param.dart +++ b/lib/pages/space_management_v2/modules/create_community/domain/param/create_community_param.dart @@ -1,9 +1,13 @@ import 'package:equatable/equatable.dart'; class CreateCommunityParam extends Equatable { - const CreateCommunityParam({required this.name}); - + const CreateCommunityParam({ + required this.name, + this.description = '', + }); + final String name; + final String description; @override List get props => [name]; diff --git a/lib/pages/space_management_v2/modules/create_community/presentation/create_community_dialog.dart b/lib/pages/space_management_v2/modules/create_community/presentation/create_community_dialog.dart new file mode 100644 index 00000000..8c1d474d --- /dev/null +++ b/lib/pages/space_management_v2/modules/create_community/presentation/create_community_dialog.dart @@ -0,0 +1,61 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/create_community/data/services/remote_create_community_service.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/create_community/presentation/bloc/create_community_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/create_community/presentation/create_community_dialog_widget.dart'; +import 'package:syncrow_web/services/api/http_service.dart'; + +class CreateCommunityDialog extends StatelessWidget { + final void Function(CommunityModel community) onCreateCommunity; + final String? initialName; + final Widget title; + + const CreateCommunityDialog({ + super.key, + required this.onCreateCommunity, + required this.title, + this.initialName, + }); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (_) => CreateCommunityBloc(RemoteCreateCommunityService(HTTPService())), + child: BlocListener( + listener: (context, state) { + switch (state) { + case CreateCommunityLoading(): + showDialog( + context: context, + builder: (context) => const Center( + child: CircularProgressIndicator(), + ), + ); + break; + case CreateCommunitySuccess(:final community): + Navigator.of(context).pop(); + Navigator.of(context).pop(); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Community created successfully')), + ); + onCreateCommunity.call(community); + break; + case CreateCommunityFailure(:final message): + Navigator.of(context).pop(); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(message)), + ); + break; + default: + break; + } + }, + child: CreateCommunityDialogWidget( + title: title, + initialName: initialName, + ), + ), + ); + } +} diff --git a/lib/pages/space_management_v2/modules/create_community/presentation/create_community_dialog_widget.dart b/lib/pages/space_management_v2/modules/create_community/presentation/create_community_dialog_widget.dart new file mode 100644 index 00000000..ab9f7b9a --- /dev/null +++ b/lib/pages/space_management_v2/modules/create_community/presentation/create_community_dialog_widget.dart @@ -0,0 +1,144 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/common/buttons/cancel_button.dart'; +import 'package:syncrow_web/pages/common/buttons/default_button.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/create_community/domain/param/create_community_param.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/create_community/presentation/bloc/create_community_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/create_community/presentation/create_community_name_text_field.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class CreateCommunityDialogWidget extends StatefulWidget { + final String? initialName; + final Widget title; + + const CreateCommunityDialogWidget({ + super.key, + required this.title, + this.initialName, + }); + + @override + State createState() => + _CreateCommunityDialogWidgetState(); +} + +class _CreateCommunityDialogWidgetState extends State { + late final TextEditingController _nameController; + + @override + void initState() { + _nameController = TextEditingController(text: widget.initialName ?? ''); + super.initState(); + } + + @override + void dispose() { + _nameController.dispose(); + super.dispose(); + } + + final _formKey = GlobalKey(); + @override + Widget build(BuildContext context) { + return Dialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + backgroundColor: ColorsManager.transparentColor, + child: Container( + width: MediaQuery.of(context).size.width * 0.3, + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: ColorsManager.whiteColors, + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: ColorsManager.blackColor.withValues(alpha: 0.25), + blurRadius: 20, + spreadRadius: 5, + offset: const Offset(0, 5), + ), + ], + ), + child: Form( + key: _formKey, + child: SingleChildScrollView( + child: BlocBuilder( + builder: (context, state) { + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + DefaultTextStyle( + style: Theme.of(context).textTheme.headlineMedium!, + child: widget.title, + ), + const SizedBox(height: 18), + CreateCommunityNameTextField( + nameController: _nameController, + ), + if (state case CreateCommunityFailure(:final message)) + Padding( + padding: const EdgeInsets.only(top: 18), + child: SelectableText( + '* $message', + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Theme.of(context).colorScheme.error, + ), + ), + ), + const SizedBox(height: 24), + _buildActionButtons(context), + ], + ); + }, + ), + ), + ), + ), + ); + } + + Widget _buildActionButtons(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: CancelButton( + label: 'Cancel', + onPressed: () => Navigator.of(context).pop(), + ), + ), + const SizedBox(width: 16), + _buildCreateCommunityButton(context), + ], + ); + } + + Widget _buildCreateCommunityButton(BuildContext context) { + return Expanded( + child: DefaultButton( + onPressed: () { + if (_formKey.currentState?.validate() ?? false) { + _onSubmit(context); + } + }, + borderRadius: 10, + foregroundColor: ColorsManager.whiteColors, + child: const Text('OK'), + ), + ); + } + + void _onSubmit(BuildContext context) { + if (_formKey.currentState?.validate() ?? false) { + context.read().add( + CreateCommunity( + CreateCommunityParam( + name: _nameController.text.trim(), + ), + ), + ); + } + } +} diff --git a/lib/pages/space_management_v2/modules/create_community/presentation/create_community_name_text_field.dart b/lib/pages/space_management_v2/modules/create_community/presentation/create_community_name_text_field.dart new file mode 100644 index 00000000..d42474d5 --- /dev/null +++ b/lib/pages/space_management_v2/modules/create_community/presentation/create_community_name_text_field.dart @@ -0,0 +1,48 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class CreateCommunityNameTextField extends StatelessWidget { + const CreateCommunityNameTextField({ + required this.nameController, + super.key, + }); + + final TextEditingController nameController; + + @override + Widget build(BuildContext context) { + return TextFormField( + controller: nameController, + validator: _validator, + style: context.textTheme.bodyMedium, + decoration: InputDecoration( + hintText: 'Please enter the community name', + filled: true, + fillColor: ColorsManager.boxColor, + enabledBorder: _buildBorder(ColorsManager.boxColor), + focusedBorder: _buildBorder(), + focusedErrorBorder: _buildBorder(Theme.of(context).colorScheme.error), + errorBorder: _buildBorder(Theme.of(context).colorScheme.error), + ), + ); + } + + String? _validator(String? value) { + if (value == null || value.isEmpty) { + return '*Name should not be empty.'; + } + + return null; + } + + InputBorder _buildBorder([Color? color]) { + return OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: BorderSide( + color: color ?? ColorsManager.vividBlue.withValues(alpha: 0.5), + width: 1, + ), + ); + } +} diff --git a/lib/pages/spaces_management/all_spaces/widgets/space_tile_widget.dart b/lib/pages/spaces_management/all_spaces/widgets/space_tile_widget.dart index d72f22ac..d81a3b04 100644 --- a/lib/pages/spaces_management/all_spaces/widgets/space_tile_widget.dart +++ b/lib/pages/spaces_management/all_spaces/widgets/space_tile_widget.dart @@ -4,11 +4,10 @@ import 'package:syncrow_web/common/widgets/custom_expansion_tile.dart'; class SpaceTile extends StatefulWidget { final String title; final bool isSelected; - final bool initiallyExpanded; final ValueChanged onExpansionChanged; final List? children; - final Function() onItemSelected; + final void Function() onItemSelected; const SpaceTile({ super.key, diff --git a/lib/utils/constants/api_const.dart b/lib/utils/constants/api_const.dart index 7e5942e3..eb7b6a3e 100644 --- a/lib/utils/constants/api_const.dart +++ b/lib/utils/constants/api_const.dart @@ -46,6 +46,7 @@ abstract class ApiEndpoints { // Community Module static const String createCommunity = '/projects/{projectId}/communities'; static const String getCommunityList = '/projects/{projectId}/communities'; + static const String getCommunityListv2 = '/projects/{projectId}/communities/v2'; static const String getCommunityById = '/projects/{projectId}/communities/{communityId}'; static const String updateCommunity =