diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 00000000..e4d616c1 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,25 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "web", + "request": "launch", + "type": "dart" + }, + { + "name": "web (profile mode)", + "request": "launch", + "type": "dart", + "flutterMode": "profile" + }, + { + "name": "web (release mode)", + "request": "launch", + "type": "dart", + "flutterMode": "release" + } + ] +} \ No newline at end of file diff --git a/assets/icons/desk_icon.svg b/assets/icons/desk_icon.svg new file mode 100644 index 00000000..04b35926 --- /dev/null +++ b/assets/icons/desk_icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift index 70693e4a..b6363034 100644 --- a/ios/Runner/AppDelegate.swift +++ b/ios/Runner/AppDelegate.swift @@ -1,7 +1,7 @@ import UIKit import Flutter -@UIApplicationMain +@main @objc class AppDelegate: FlutterAppDelegate { override func application( _ application: UIApplication, diff --git a/lib/pages/common/buttons/add_space_button.dart b/lib/pages/common/buttons/add_space_button.dart new file mode 100644 index 00000000..8348b390 --- /dev/null +++ b/lib/pages/common/buttons/add_space_button.dart @@ -0,0 +1,44 @@ +import 'package:flutter/material.dart'; + +class AddSpaceButton extends StatelessWidget { + final VoidCallback onTap; + + const AddSpaceButton({super.key, required this.onTap}); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onTap, // Handle tap event + child: Container( + width: 120, // Width of the button + height: 60, // Height of the button + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), // Rounded corners + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.5), // Shadow color + spreadRadius: 5, // Spread radius of the shadow + blurRadius: 7, // Blur effect + offset: const Offset(0, 3), // Shadow position + ), + ], + ), + child: Center( + child: Container( + width: 40, // Size of the inner circle + height: 40, + decoration: BoxDecoration( + color: const Color(0xFFF5F6F7), // Light gray background + shape: BoxShape.circle, // Circular shape for the icon container + ), + child: const Icon( + Icons.add, // Add icon + color: Colors.blue, // Icon color + ), + ), + ), + ), + ); + } +} diff --git a/lib/pages/common/buttons/cancel_button.dart b/lib/pages/common/buttons/cancel_button.dart new file mode 100644 index 00000000..da6dcdc7 --- /dev/null +++ b/lib/pages/common/buttons/cancel_button.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class CancelButton extends StatelessWidget { + final String label; + final VoidCallback? onPressed; + final double? height; // Optional height parameter for customization + final double? borderRadius; // Optional border radius customization + final double? width; + + const CancelButton({ + super.key, + required this.label, // Button label + required this.onPressed, // Button action + this.height = 40, // Default height + this.width = 140, + this.borderRadius = 10, // Default border radius + }); + + @override + Widget build(BuildContext context) { + return ElevatedButton( + onPressed: onPressed, + style: ButtonStyle( + backgroundColor: WidgetStateProperty.all(ColorsManager.boxColor), // White background + foregroundColor: WidgetStateProperty.all(Colors.black), // Black text color + shape: WidgetStateProperty.all( + RoundedRectangleBorder( + borderRadius: BorderRadius.circular(borderRadius ?? 10), + side: const BorderSide(color: ColorsManager.boxColor), // Black border + ), + ), + fixedSize: WidgetStateProperty.all(Size(width ?? 50, height ?? 40)), // Set button height + ), + child: Text(label), // Dynamic label + ); + } +} \ No newline at end of file diff --git a/lib/pages/common/buttons/default_button.dart b/lib/pages/common/buttons/default_button.dart index 5aa506f8..37320b26 100644 --- a/lib/pages/common/buttons/default_button.dart +++ b/lib/pages/common/buttons/default_button.dart @@ -15,7 +15,8 @@ class DefaultButton extends StatelessWidget { this.backgroundColor, this.foregroundColor, this.borderRadius, - this.height, + this.height = 40, + this.width = 140, this.padding, }); final void Function()? onPressed; @@ -31,6 +32,8 @@ class DefaultButton extends StatelessWidget { final ButtonStyle? customButtonStyle; final Color? backgroundColor; final Color? foregroundColor; + final double? width; + @override Widget build(BuildContext context) { return ElevatedButton( @@ -39,6 +42,7 @@ class DefaultButton extends StatelessWidget { ? null : customButtonStyle ?? ButtonStyle( + fixedSize: WidgetStateProperty.all(Size(width ?? 50, height ?? 40)), // Set button height textStyle: MaterialStateProperty.all( customTextStyle ?? Theme.of(context).textTheme.bodySmall!.copyWith( @@ -59,14 +63,11 @@ class DefaultButton extends StatelessWidget { ? backgroundColor ?? ColorsManager.primaryColor : Colors.black.withOpacity(0.2); }), - shape: MaterialStateProperty.all( + shape: WidgetStateProperty.all( RoundedRectangleBorder( - borderRadius: BorderRadius.circular(borderRadius ?? 20), + borderRadius: BorderRadius.circular(borderRadius ?? 10), ), ), - fixedSize: MaterialStateProperty.all( - const Size.fromHeight(50), - ), padding: MaterialStateProperty.all( EdgeInsets.all(padding ?? 10), ), diff --git a/lib/pages/spaces_management/bloc/space_management_bloc.dart b/lib/pages/spaces_management/bloc/space_management_bloc.dart new file mode 100644 index 00000000..bb4afe19 --- /dev/null +++ b/lib/pages/spaces_management/bloc/space_management_bloc.dart @@ -0,0 +1,60 @@ +import 'package:equatable/equatable.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import 'package:syncrow_web/pages/spaces_management/model/space_model.dart'; + +// Events +abstract class SpaceEvent extends Equatable { + @override + List get props => []; +} + +class LoadSpaces extends SpaceEvent {} + +class SelectSpace extends SpaceEvent { + final String selectedSpace; + + SelectSpace(this.selectedSpace); + + @override + List get props => [selectedSpace]; +} + +// States +abstract class SpaceState extends Equatable { + @override + List get props => []; +} + +class SpaceInitial extends SpaceState {} + +class SpaceLoaded extends SpaceState { + final List spaces; + final String selectedSpace; + + SpaceLoaded({required this.spaces, required this.selectedSpace}); + + @override + List get props => [spaces, selectedSpace]; +} + +// Bloc +class SpacesManagementBloc extends Bloc { + SpacesManagementBloc() : super(SpaceInitial()) { + on((event, emit) { + final List spaces = [ + SpaceModel(communityName: 'Downtown Dubai', subSpaces: ['Sub Space 1', 'Sub Space 2']), + SpaceModel(communityName: 'Dubai Creek Harbour', subSpaces: ['Sub Space 1', 'Sub Space 2']), + SpaceModel(communityName: 'Dubai Hills Estate', subSpaces: ['Sub Space 1', 'Sub Space 2']), + ]; + emit(SpaceLoaded(spaces: spaces, selectedSpace: spaces[0].communityName)); + }); + + on((event, emit) { + if (state is SpaceLoaded) { + final loadedState = state as SpaceLoaded; + emit(SpaceLoaded(spaces: loadedState.spaces, selectedSpace: event.selectedSpace)); + } + }); + } +} diff --git a/lib/pages/spaces_management/model/space_model.dart b/lib/pages/spaces_management/model/space_model.dart new file mode 100644 index 00000000..ee488441 --- /dev/null +++ b/lib/pages/spaces_management/model/space_model.dart @@ -0,0 +1,6 @@ +class SpaceModel { + final String communityName; + final List subSpaces; + + SpaceModel({required this.communityName, required this.subSpaces}); +} diff --git a/lib/pages/spaces_management/view/dialogs/create_space_dialog.dart b/lib/pages/spaces_management/view/dialogs/create_space_dialog.dart new file mode 100644 index 00000000..d80de241 --- /dev/null +++ b/lib/pages/spaces_management/view/dialogs/create_space_dialog.dart @@ -0,0 +1,198 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.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/utils/color_manager.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; + +class CreateSpaceDialog extends StatefulWidget { + // Add the onCreateSpace parameter as a required field + final Function(String, String) onCreateSpace; + + const CreateSpaceDialog({super.key, required this.onCreateSpace}); + + @override + CreateSpaceDialogState createState() => CreateSpaceDialogState(); +} + +class CreateSpaceDialogState extends State { + String selectedIcon = Assets.location; // Initially selected icon + String enteredName = ''; // Store entered space name + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: const Text('Create New Space'), + backgroundColor: ColorsManager.whiteColors, + content: SizedBox( + width: 600, // Set width for the dialog + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Stack( + alignment: Alignment.center, + children: [ + Container( + width: 100, + height: 100, + decoration: const BoxDecoration( + color: Color(0xFFF5F6F7), + shape: BoxShape.circle, + ), + ), + SvgPicture.asset( + selectedIcon, // Display the selected icon here + width: 60, + height: 60, + ), + Positioned( + top: 2, + left: 2, + child: InkWell( + onTap: () => + _showIconSelectionDialog(), // Open the icon selection dialog + child: Container( + width: 20, + height: 20, + decoration: const BoxDecoration( + color: Colors.white, + shape: BoxShape.circle, + ), + child: SvgPicture.asset(Assets.iconEdit, + width: 10, height: 10), + ), + ), + ), + ], + ), + const SizedBox(width: 16), + Expanded( + child: TextField( + onChanged: (value) { + enteredName = value; // Capture entered name + }, + decoration: InputDecoration( + hintText: 'Please enter the name', + filled: true, + fillColor: const Color(0xFFF5F6F7), + border: OutlineInputBorder( + borderSide: const BorderSide(color: Color(0xFFF5F6F7)), + borderRadius: BorderRadius.circular(10), + ), + enabledBorder: OutlineInputBorder( + borderSide: BorderSide( + color: Color(0xFFF5F6F7), + width: 1), // Default border + borderRadius: BorderRadius.circular(10), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: + BorderSide(color: Color(0xFFF5F6F7), width: 1), + ), + ), + ), + ), + ], + ), + ], + ), + ), + actions: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: CancelButton( + label: 'Cancel', + onPressed: () => Navigator.of(context).pop(), + ), + ), + const SizedBox(width: 10), + Expanded( + child: DefaultButton( + onPressed: () { + if (enteredName.isNotEmpty) { + widget.onCreateSpace(enteredName, selectedIcon); // Pass name and icon back + Navigator.of(context).pop(); // Close dialog + } + }, + child: const Text('OK'), + backgroundColor: const Color(0xFF023DFE), + foregroundColor: Colors.white, + ), + ), + ], + ), + ], + ); + } + + // Icon selection dialog + void _showIconSelectionDialog() { + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: const Text('Select Icon'), + backgroundColor: Colors.white, + content: Container( + width: 500, // Width of the icon selection dialog + height: 200, // Height of the dialog + padding: const EdgeInsets.all(18), + decoration: BoxDecoration( + color: const Color(0xFFF5F6F7), + borderRadius: BorderRadius.circular(12), + ), + child: GridView.builder( + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 7, // Number of icons per row + crossAxisSpacing: 10, // Space between icons horizontally + mainAxisSpacing: 22, // Space between icons vertically + ), + itemCount: _iconList.length, + itemBuilder: (BuildContext context, int index) { + return GestureDetector( + onTap: () { + setState(() { + selectedIcon = + _iconList[index]; // Update the selected icon + }); + Navigator.of(context) + .pop(); // Close the icon selection dialog + }, + child: SvgPicture.asset( + _iconList[index], + width: 50, // Adjust size as needed + height: 50, + ), + ); + }, + ), + ), + ); + }, + ); + } + + // Icon list containing SVG asset paths + final List _iconList = [ + Assets.location, + Assets.villa, + Assets.gym, + Assets.sauna, + Assets.bbq, + Assets.building, + Assets.desk, + Assets.door, + Assets.parking, + Assets.pool, + Assets.stair, + Assets.steamRoom, + Assets.street, + Assets.unit, + ]; +} diff --git a/lib/pages/spaces_management/view/space_widget.dart b/lib/pages/spaces_management/view/space_widget.dart new file mode 100644 index 00000000..0d661292 --- /dev/null +++ b/lib/pages/spaces_management/view/space_widget.dart @@ -0,0 +1,47 @@ +import 'package:flutter/material.dart'; + +class SpaceWidget extends StatelessWidget { + final String name; + final Offset position; + final VoidCallback onTap; + + const SpaceWidget({ + super.key, + required this.name, + required this.position, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + return Positioned( + left: position.dx, + top: position.dy, + child: GestureDetector( + onTap: onTap, + child: Container( + padding: const EdgeInsets.all(8.0), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(8), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.5), + spreadRadius: 5, + blurRadius: 7, + offset: const Offset(0, 3), + ), + ], + ), + child: Row( + children: [ + const Icon(Icons.location_on, color: Colors.blue), + const SizedBox(width: 8), + Text(name, style: const TextStyle(fontSize: 16)), + ], + ), + ), + ), + ); + } +} diff --git a/lib/pages/spaces_management/view/spaces_management_page.dart b/lib/pages/spaces_management/view/spaces_management_page.dart new file mode 100644 index 00000000..e185ee7d --- /dev/null +++ b/lib/pages/spaces_management/view/spaces_management_page.dart @@ -0,0 +1,203 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:syncrow_web/pages/common/buttons/add_space_button.dart'; +import 'package:syncrow_web/pages/spaces_management/view/dialogs/create_space_dialog.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class SpaceManagementPage extends StatefulWidget { + @override + SpaceManagementPageState createState() => SpaceManagementPageState(); +} + +class SpaceManagementPageState extends State { + // Store created spaces + List spaces = []; + + @override + Widget build(BuildContext context) { + Size screenSize = MediaQuery.of(context).size; + + return Scaffold( + backgroundColor: ColorsManager.whiteColors, + appBar: AppBar( + title: const Text('Space Management'), + ), + body: Stack( + children: [ + Center( + child: spaces.isEmpty + ? AddSpaceButton( + onTap: () { + _showCreateSpaceDialog(screenSize); + }, + ) + : Stack( + children: spaces + .asMap() + .entries + .map((entry) => _buildSpaceCard(entry.key, screenSize)) + .toList(), + ), + ), + ], + ), + ); + } + + // Function to open the Create Space dialog + void _showCreateSpaceDialog(Size screenSize, {Offset? position}) { + showDialog( + context: context, + builder: (BuildContext context) { + return CreateSpaceDialog( + onCreateSpace: (String name, String icon) { + setState(() { + // Set the first space in the center or use passed position + Offset centerPosition = position ?? + Offset( + screenSize.width / 2 - 75, // Center horizontally + screenSize.height / 2 - 100, // Slightly above the center vertically + ); + + spaces.add(SpaceData(name: name, icon: icon, position: centerPosition)); + }); + }, + ); + }, + ); + } + + // Function to build a draggable space card + Widget _buildSpaceCard(int index, Size screenSize) { + return Positioned( + left: spaces[index].position.dx, + top: spaces[index].position.dy, + child: GestureDetector( + onPanUpdate: (details) { + // Update the position of the space card while dragging + setState(() { + spaces[index].position += details.delta; + }); + }, + child: MouseRegion( + onEnter: (_) { + // Show plus buttons on hover + setState(() { + spaces[index].isHovered = true; + }); + }, + onExit: (_) { + // Hide plus buttons when not hovered + setState(() { + spaces[index].isHovered = false; + }); + }, + child: Stack( + clipBehavior: Clip.none, + children: [ + _buildSpaceContainer(index), + if (spaces[index].isHovered) ...[ + _buildPlusButton(index, 'left', const Offset(-21, 20), screenSize), + _buildPlusButton(index, 'right', const Offset(140, 20), screenSize), + _buildPlusButton(index, 'down', const Offset(63, 55), screenSize), + ], + ], + ), + ), + ), + ); + } + + // Function to build the space container with the styled format + Widget _buildSpaceContainer(int index) { + return Container( + width: 150, + height: 60, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(15), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.5), + spreadRadius: 2, + blurRadius: 5, + offset: const Offset(0, 3), + ), + ], + ), + child: Row( + children: [ + Container( + width: 40, + height: 60, + decoration: const BoxDecoration( + color: Color(0xFF023DFE), + borderRadius: BorderRadius.only( + topLeft: Radius.circular(15), + bottomLeft: Radius.circular(15), + ), + ), + child: Center( + child: SvgPicture.asset(spaces[index].icon, width: 24, height: 24, color: Colors.white), + ), + ), + const SizedBox(width: 10), + Text( + spaces[index].name, + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + ), + ), + ], + ), + ); + } + + // Function to build plus buttons for new space creation + Widget _buildPlusButton(int index, String direction, Offset offset, Size screenSize) { + return Positioned( + left: offset.dx, + top: offset.dy, + child: GestureDetector( + onTap: () { + Offset newPosition; + switch (direction) { + case 'left': + newPosition = spaces[index].position + const Offset(-200, 0); + break; + case 'right': + newPosition = spaces[index].position + const Offset(200, 0); + break; + case 'down': + newPosition = spaces[index].position + const Offset(0, 150); + break; + default: + newPosition = spaces[index].position; + } + + _showCreateSpaceDialog(screenSize, position: newPosition); + }, + child: Container( + width: 30, + height: 30, + decoration: const BoxDecoration( + color: Color(0xFF023DFE), + shape: BoxShape.circle, + ), + child: const Icon(Icons.add, color: Colors.white, size: 20), + ), + ), + ); + } +} + +// Model for storing space information +class SpaceData { + final String name; + final String icon; + Offset position; + bool isHovered; + + SpaceData({required this.name, required this.icon, required this.position, this.isHovered = false}); +} diff --git a/lib/utils/app_routes.dart b/lib/utils/app_routes.dart index a730341b..3714aa69 100644 --- a/lib/utils/app_routes.dart +++ b/lib/utils/app_routes.dart @@ -3,6 +3,7 @@ import 'package:syncrow_web/pages/access_management/view/access_management.dart' import 'package:syncrow_web/pages/auth/view/login_page.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/view/device_managment_page.dart'; import 'package:syncrow_web/pages/home/view/home_page.dart'; +import 'package:syncrow_web/pages/spaces_management/view/spaces_management_page.dart'; import 'package:syncrow_web/pages/visitor_password/view/visitor_password_dialog.dart'; import 'package:syncrow_web/utils/constants/routes_const.dart'; @@ -11,7 +12,7 @@ class AppRoutes { return [ GoRoute( path: RoutesConst.auth, - builder: (context, state) => const LoginPage(), + builder: (context, state) => SpaceManagementPage(), ), GoRoute( path: RoutesConst.home, @@ -29,6 +30,9 @@ class AppRoutes { path: RoutesConst.deviceManagementPage, builder: (context, state) => const DeviceManagementPage(), ), + GoRoute( + path: RoutesConst.spacesManagementPage, + builder: (context, state) => SpaceManagementPage()), ]; } } diff --git a/pubspec.lock b/pubspec.lock index 9a9cd6a8..7973e388 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -268,18 +268,18 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "7f0df31977cb2c0b88585095d168e689669a2cc9b97c309665e3386f3e9d341a" + sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" url: "https://pub.dev" source: hosted - version: "10.0.4" + version: "10.0.5" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "06e98f569d004c1315b991ded39924b21af84cf14cc94791b8aea337d25b57f8" + sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" url: "https://pub.dev" source: hosted - version: "3.0.3" + version: "3.0.5" leak_tracker_testing: dependency: transitive description: @@ -316,18 +316,18 @@ packages: dependency: transitive description: name: material_color_utilities - sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec url: "https://pub.dev" source: hosted - version: "0.8.0" + version: "0.11.1" meta: dependency: transitive description: name: meta - sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136" + sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 url: "https://pub.dev" source: hosted - version: "1.12.0" + version: "1.15.0" nested: dependency: transitive description: @@ -537,10 +537,10 @@ packages: dependency: transitive description: name: test_api - sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f" + sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" url: "https://pub.dev" source: hosted - version: "0.7.0" + version: "0.7.2" typed_data: dependency: transitive description: @@ -585,10 +585,10 @@ packages: dependency: transitive description: name: vm_service - sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec" + sha256: f652077d0bdf60abe4c1f6377448e8655008eef28f128bc023f7b5e8dfeb48fc url: "https://pub.dev" source: hosted - version: "14.2.1" + version: "14.2.4" web: dependency: transitive description: