diff --git a/assets/icons/arrow_up.svg b/assets/icons/arrow_up.svg
new file mode 100644
index 0000000..667369c
--- /dev/null
+++ b/assets/icons/arrow_up.svg
@@ -0,0 +1,4 @@
+
diff --git a/assets/icons/arrow_up_right.svg b/assets/icons/arrow_up_right.svg
new file mode 100644
index 0000000..ba2fd0d
--- /dev/null
+++ b/assets/icons/arrow_up_right.svg
@@ -0,0 +1,4 @@
+
diff --git a/assets/icons/cancel_circle.svg b/assets/icons/cancel_circle.svg
new file mode 100644
index 0000000..f9cdf56
--- /dev/null
+++ b/assets/icons/cancel_circle.svg
@@ -0,0 +1,5 @@
+
diff --git a/assets/icons/canvas.svg b/assets/icons/canvas.svg
new file mode 100644
index 0000000..e65a06d
--- /dev/null
+++ b/assets/icons/canvas.svg
@@ -0,0 +1,5 @@
+
diff --git a/assets/icons/filter.svg b/assets/icons/filter.svg
new file mode 100644
index 0000000..0f119ac
--- /dev/null
+++ b/assets/icons/filter.svg
@@ -0,0 +1,7 @@
+
diff --git a/assets/icons/plug.svg b/assets/icons/plug.svg
new file mode 100644
index 0000000..f60cab0
--- /dev/null
+++ b/assets/icons/plug.svg
@@ -0,0 +1,5 @@
+
diff --git a/lib/di/injection.dart b/lib/di/injection.dart
index d3b4bb0..4e87306 100644
--- a/lib/di/injection.dart
+++ b/lib/di/injection.dart
@@ -33,6 +33,7 @@ import 'package:share_space/domain/usecase/workspace/save_workspace.dart';
import 'package:share_space/presentation/screen/booking_history/state/booking_history_cubit.dart';
import 'package:share_space/presentation/screen/home/state/home_cubit.dart';
import 'package:share_space/presentation/screen/login/state/login_cubit.dart';
+import 'package:share_space/presentation/screen/search/state/search_cubit.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../data/remote/auth_api_service_impl.dart';
@@ -164,6 +165,8 @@ Future setupDependencies() async {
getIt.registerFactory(() => BookingCubit(getIt(),getIt()));
+ getIt.registerFactory(() => SearchCubit(getIt(), getIt()));
+
getIt.registerFactory(
() => CreateAccountUseCase(getIt()),
);
diff --git a/lib/presentation/design_system/widget/base_bottom_sheet.dart b/lib/presentation/design_system/widget/base_bottom_sheet.dart
new file mode 100644
index 0000000..35d5f1f
--- /dev/null
+++ b/lib/presentation/design_system/widget/base_bottom_sheet.dart
@@ -0,0 +1,109 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_svg/flutter_svg.dart';
+import 'package:share_space/presentation/design_system/theme/app_theme_provider.dart';
+
+import '../theme/app_theme.dart';
+
+class BaseBottomSheet extends StatefulWidget {
+ final Widget child;
+ final String label;
+ final Function() onClose;
+
+ const BaseBottomSheet({
+ super.key,
+ required this.child,
+ required this.label,
+ required this.onClose,
+ });
+
+ @override
+ State createState() => _BaseBottomSheetState();
+}
+
+class _BaseBottomSheetState extends State {
+ @override
+ Widget build(BuildContext context) {
+ return AppThemeProvider(
+ child: Container(
+ color: AppTheme.of(context).colors.surface,
+ child: Column(
+ children: [
+ Padding(
+ padding: EdgeInsetsGeometry.only(left: 16, right: 16, top: 24),
+ child: Column(
+ children: [
+ Row(
+ children: [
+ Text(
+ widget.label,
+ style: AppTheme.of(context)
+ .typography
+ .textTheme
+ .titleSmall
+ ?.copyWith(
+ color: AppTheme.of(context).colors.title,
+ ),
+ ),
+ Expanded(child: SizedBox()),
+ SizedBox(
+ width: 20,
+ height: 20,
+ child: IconButton(
+ padding: EdgeInsets.zero,
+ alignment: Alignment.center,
+ icon: SvgPicture.asset(
+ 'assets/icons/cancel_circle.svg',
+ ),
+ onPressed: () {
+ widget.onClose();
+ Navigator.pop(context);
+ },
+ ),
+ ),
+ ],
+ ),
+ SizedBox(height: 12),
+ Divider(
+ color: AppTheme.of(context).colors.stroke,
+ thickness: 1,
+ height: 0,
+ ),
+ ],
+ ),
+ ),
+ widget.child,
+ ],
+ ),
+ ),
+ );
+ }
+}
+
+Future showBaseBottomSheet({
+ required BuildContext context,
+ required Widget child,
+ required String label,
+ required double height,
+ VoidCallback? onClose,
+ bool isScrollControlled = true,
+}) {
+ return showModalBottomSheet(
+ isScrollControlled: isScrollControlled,
+ context: context,
+ backgroundColor: AppTheme.of(context).colors.surface,
+ builder: (BuildContext context) {
+ return AppThemeProvider(
+ child: SafeArea(
+ child: SizedBox(
+ height: MediaQuery.of(context).size.height * height,
+ child: BaseBottomSheet(
+ label: label,
+ onClose: onClose ?? () {},
+ child: child,
+ ),
+ ),
+ ),
+ );
+ },
+ );
+}
diff --git a/lib/presentation/design_system/widget/primary_button.dart b/lib/presentation/design_system/widget/primary_button.dart
new file mode 100644
index 0000000..058501e
--- /dev/null
+++ b/lib/presentation/design_system/widget/primary_button.dart
@@ -0,0 +1,60 @@
+import 'package:flutter/material.dart';
+import '../theme/app_theme.dart';
+
+class PrimaryButton extends StatefulWidget {
+ final Function() onPressed;
+ final String label;
+ final bool isBig;
+ bool isDisabled;
+
+ PrimaryButton({
+ super.key,
+ required this.onPressed,
+ required this.label,
+ this.isBig = true,
+ this.isDisabled = false,
+ });
+
+ @override
+ State createState() => _PrimaryButtonState();
+}
+
+class _PrimaryButtonState extends State {
+ @override
+ Widget build(BuildContext context) {
+ final theme = AppTheme.of(context);
+ return ElevatedButton(
+ style: ButtonStyle(
+ padding: WidgetStateProperty.all(EdgeInsets.zero),
+ shape: WidgetStateProperty.all(
+ RoundedRectangleBorder(borderRadius: BorderRadius.circular(50)),
+ ),
+ shadowColor: WidgetStateProperty.all(Colors.transparent),
+ ),
+ onPressed: widget.isDisabled ? null : widget.onPressed,
+ child: AnimatedContainer(
+ duration: const Duration(milliseconds: 150),
+ height: widget.isBig ? 52 : 32,
+ alignment: Alignment.center,
+ decoration: BoxDecoration(
+ gradient: LinearGradient(
+ begin: Alignment.topCenter,
+ end: Alignment.bottomCenter,
+ colors: widget.isDisabled
+ ? [theme.colors.disable, theme.colors.disable]
+ : [Color(0xFF50B5E7), Color(0xFF19C6F9)],
+ ),
+ borderRadius: BorderRadius.circular(100),
+ ),
+ child: Text(
+ widget.label,
+ style: theme.typography.textTheme.labelMedium?.copyWith(
+ color: theme.colors.onPrimary,
+ fontWeight: FontWeight.w500,
+ ),
+ ),
+ ),
+ );
+ ;
+ }
+}
diff --git a/lib/presentation/design_system/widget/secondary_button.dart b/lib/presentation/design_system/widget/secondary_button.dart
new file mode 100644
index 0000000..7010bf9
--- /dev/null
+++ b/lib/presentation/design_system/widget/secondary_button.dart
@@ -0,0 +1,59 @@
+import 'package:flutter/material.dart';
+import '../theme/app_theme.dart';
+
+class SecondaryButton extends StatefulWidget {
+ final Function() onPressed;
+ final String label;
+ final bool isBig;
+ bool isDisabled;
+
+ SecondaryButton({
+ super.key,
+ required this.onPressed,
+ required this.label,
+ this.isBig = true,
+ this.isDisabled = false,
+ });
+
+ @override
+ State createState() => _SecondaryButtonState();
+}
+
+class _SecondaryButtonState extends State {
+ @override
+ Widget build(BuildContext context) {
+ final theme = AppTheme.of(context);
+ return ElevatedButton(
+ style: ButtonStyle(
+ padding: WidgetStateProperty.all(EdgeInsets.zero),
+ shape: WidgetStateProperty.all(
+ RoundedRectangleBorder(borderRadius: BorderRadius.circular(50)),
+ ),
+ shadowColor: WidgetStateProperty.all(Colors.transparent),
+ ),
+ onPressed: widget.isDisabled ? null : widget.onPressed,
+ child: AnimatedContainer(
+ duration: const Duration(milliseconds: 150),
+ height: widget.isBig ? 52 : 32,
+ alignment: Alignment.center,
+ decoration: BoxDecoration(
+ color: widget.isDisabled
+ ? theme.colors.blueVariant
+ : theme.colors.disable,
+ borderRadius: BorderRadius.circular(100),
+ border: Border.all(width: 0.5, color: theme.colors.stroke),
+ ),
+ child: Text(
+ widget.label,
+ style: theme.typography.textTheme.labelMedium?.copyWith(
+ color: widget.isDisabled
+ ? theme.colors.primary
+ : theme.colors.onPrimary,
+ fontWeight: FontWeight.w500,
+ ),
+ ),
+ ),
+ );
+ ;
+ }
+}
diff --git a/lib/presentation/screen/booking_history/booking_history_screen.dart b/lib/presentation/screen/booking_history/booking_history_screen.dart
index 8cabc78..124a9a5 100644
--- a/lib/presentation/screen/booking_history/booking_history_screen.dart
+++ b/lib/presentation/screen/booking_history/booking_history_screen.dart
@@ -13,6 +13,7 @@ import 'package:share_space/presentation/screen/shared/ui_state/workspace_ui_sta
import '../../../resources/app_strings.dart';
import '../../design_system/widget/custom_top_snackbar.dart';
+import '../../design_system/widget/error_screen.dart';
class BookingHistoryScreen extends StatefulWidget {
const BookingHistoryScreen({super.key});
@@ -142,12 +143,7 @@ class _BookingHistoryScreenState extends State {
),
);
} else if (state is BookingHistoryError) {
- return Center(
- child: Text(
- state.message,
- style: theme.typography.textTheme.labelSmall,
- ),
- );
+ return const ErrorScreen(hasAppBar: false);
}
return const SizedBox.shrink();
},
diff --git a/lib/presentation/screen/search/filter_bottomsheet.dart b/lib/presentation/screen/search/filter_bottomsheet.dart
new file mode 100644
index 0000000..1a6d179
--- /dev/null
+++ b/lib/presentation/screen/search/filter_bottomsheet.dart
@@ -0,0 +1,191 @@
+import 'package:flutter/material.dart';
+import 'package:share_space/presentation/design_system/theme/app_theme.dart';
+import 'package:share_space/presentation/screen/search/widget/range_chip.dart';
+import 'package:share_space/presentation/screen/search/widget/range_slider.dart';
+import 'package:share_space/presentation/screen/search/widget/rating_filter.dart';
+import 'package:share_space/presentation/screen/search/widget/services_filter.dart';
+import 'package:share_space/presentation/screen/shared/ui_state/workspace_ui_state.dart';
+
+import '../../design_system/widget/primary_button.dart';
+import '../../design_system/widget/secondary_button.dart';
+
+class FilterBottomSheet extends StatefulWidget {
+ final RangeValues initialRange;
+ RangeValues currentRange;
+ final List rateOptions;
+ final int rateSelectedIndex;
+ List selectedRateIndices;
+ final List servicesOptions;
+ List selectedServicesIndices;
+
+ FilterBottomSheet({
+ super.key,
+ this.initialRange = const RangeValues(10, 500),
+ required this.currentRange,
+ required this.rateSelectedIndex,
+ this.rateOptions = const ["All", "1", "2", "3", "4", "5"],
+ required this.selectedRateIndices,
+ this.servicesOptions = ServicesUiState.values,
+ required this.selectedServicesIndices,
+ });
+
+ @override
+ State createState() => _FilterBottomSheetState();
+}
+
+class _FilterBottomSheetState extends State {
+
+ @override
+ Widget build(BuildContext context) {
+ final theme = AppTheme.of(context);
+ return Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
+ child: Column(
+ children: [
+ Align(
+ alignment: Alignment.topLeft,
+ child: Text(
+ "Rate",
+ style: theme.typography.textTheme.titleSmall?.copyWith(
+ color: theme.colors.title,
+ ),
+ ),
+ ),
+ SizedBox(height: 12),
+ RatingFilter(
+ options: widget.rateOptions,
+ selectedIndices: widget.selectedRateIndices,
+ onSelect: (index) {
+ setState(() {
+ if (widget.selectedRateIndices.contains(index)) {
+ widget.selectedRateIndices.remove(index);
+ if (widget.selectedRateIndices.isEmpty) {
+ widget.selectedRateIndices.add(0);
+ }
+ } else {
+ if (index == 0) {
+ widget.selectedRateIndices.clear();
+ }
+ if (widget.selectedRateIndices.length == 1 &&
+ widget.selectedRateIndices.first == 0) {
+ widget.selectedRateIndices.clear();
+ }
+ widget.selectedRateIndices.add(index);
+ }
+ });
+ },
+ ),
+ SizedBox(height: 16),
+ Align(
+ alignment: Alignment.topLeft,
+ child: Text(
+ "Price Range",
+ style: theme.typography.textTheme.titleSmall?.copyWith(
+ color: theme.colors.title,
+ ),
+ ),
+ ),
+ SizedBox(height: 12),
+ RangeSliderWidget(
+ maxRange: widget.initialRange,
+ currentRange: widget.currentRange,
+ onChange: (newRange) {
+ setState(() {
+ widget.currentRange = newRange;
+ });
+ },
+ currency: "/hr",
+ ),
+ SizedBox(height: 20),
+ Row(
+ children: [
+ Expanded(
+ child: RangeChip(
+ label: "Min",
+ value: widget.currentRange.start.toInt(),
+ image: "assets/icons/arrow_down.svg",
+ unit: "IQD",
+ ),
+ ),
+ const SizedBox(width: 8),
+ Expanded(
+ child: RangeChip(
+ label: "Max",
+ value: widget.currentRange.end.toInt(),
+ image: "assets/icons/arrow_up.svg",
+ unit: "IQD",
+ ),
+ ),
+ ],
+ ),
+ SizedBox(height: 16),
+ Align(
+ alignment: Alignment.topLeft,
+ child: Text(
+ "Services",
+ style: theme.typography.textTheme.titleSmall?.copyWith(
+ color: theme.colors.title,
+ ),
+ ),
+ ),
+ SizedBox(height: 12),
+ ServicesFilter(
+ options: widget.servicesOptions.map((e) => e.toString()).toList(),
+ selectedIndices: widget.selectedServicesIndices,
+ onSelect: (index) {
+ setState(() {
+ if (widget.selectedServicesIndices.contains(index)) {
+ widget.selectedServicesIndices.remove(index);
+ if (widget.selectedServicesIndices.isEmpty) {
+ widget.selectedServicesIndices.add(0);
+ }
+ } else {
+ if (index == 0) {
+ widget.selectedServicesIndices.clear();
+ }
+ if (widget.selectedServicesIndices.length == 1 &&
+ widget.selectedServicesIndices.first == 0) {
+ widget.selectedServicesIndices.clear();
+ }
+ widget.selectedServicesIndices.add(index);
+ }
+ });
+ },
+ ),
+ // SizedBox(height: 16),
+ // Align(
+ // alignment: Alignment.topLeft,
+ // child: Text(
+ // "Location",
+ // style: theme.typography.textTheme.titleSmall?.copyWith(
+ // color: theme.colors.title,
+ // ),
+ // ),
+ // ),
+ SizedBox(height: 12),
+ // TODO: Add Location Filter Widget here
+ SizedBox(height: 24),
+ Row(
+ mainAxisSize: MainAxisSize.max,
+ children: [
+ Expanded(
+ child: SecondaryButton(
+ onPressed: () {},
+ label: "Clear",
+ isDisabled: true,
+ ),
+ ),
+ SizedBox(width: 8),
+ Expanded(
+ child: PrimaryButton(
+ onPressed: () {},
+ label: "Apply Filters",
+ ),
+ ),
+ ],
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/lib/presentation/screen/search/search_screen.dart b/lib/presentation/screen/search/search_screen.dart
index 6a4a003..ee117b1 100644
--- a/lib/presentation/screen/search/search_screen.dart
+++ b/lib/presentation/screen/search/search_screen.dart
@@ -1,13 +1,142 @@
+import 'dart:ffi';
+
import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:share_space/presentation/screen/search/state/search_cubit.dart';
+import 'package:share_space/presentation/screen/search/state/search_state.dart';
+import 'package:share_space/presentation/screen/search/widget/search_field.dart';
import 'package:share_space/resources/app_strings.dart';
-class SearchScreen extends StatelessWidget {
+import '../../../di/injection.dart';
+import '../../design_system/theme/app_theme.dart';
+import '../../design_system/widget/error_screen.dart';
+import '../../design_system/widget/loading_screen.dart';
+import '../../design_system/widget/workspace_details_card.dart';
+import '../../routes/routes.dart';
+
+class SearchScreen extends StatefulWidget {
const SearchScreen({super.key});
+ @override
+ State createState() => _SearchScreenState();
+}
+
+class _SearchScreenState extends State {
@override
Widget build(BuildContext context) {
- return const Scaffold(
- body: Center(child: Text(AppStrings.searchScreenTitle)),
+ final theme = AppTheme.of(context);
+ return Material(
+ color: theme.colors.surfaceLow,
+ child: BlocProvider(
+ create: (context) => getIt()..fetchLastViewed(),
+ child: BlocBuilder(
+ builder: (context, state) {
+ if (state is SearchLoading) {
+ return LoadingScreen();
+ } else if (state is SearchLoaded) {
+ final rooms = (state.searchResults.isEmpty)
+ ? state.lastViewed
+ : state.searchResults;
+
+ return CustomScrollView(
+ slivers: [
+ SliverAppBar(
+ leading: const SizedBox.shrink(),
+ leadingWidth: 0,
+ title: Text(
+ AppStrings.searchScreenTitle,
+ style: theme.typography.textTheme.titleMedium?.copyWith(
+ color: theme.colors.title,
+ ),
+ ),
+ backgroundColor: theme.colors.surface,
+ surfaceTintColor: theme.colors.surface,
+ elevation: 0,
+ ),
+ SliverToBoxAdapter(
+ child: SearchField(
+ onQueryChanged: (query) {
+ context.read().updateQuery(query);
+ },
+ suggestions: [],
+ history: [],
+ initialRange: RangeValues(state.priceStartRange.toDouble() , state.priceEndRange.toDouble()),
+ currentRange: RangeValues(state.priceStart.toDouble() , state.priceEnd.toDouble()),
+ rateOptions: const ["All", "1", "2", "3", "4", "5"],
+ rateSelectedIndex: state.selectedRate,
+ selectedRateIndices: [0],
+ selectedServicesIndices: [0],
+ ),
+ ),
+ SliverToBoxAdapter(
+ child: Padding(
+ padding: const EdgeInsets.symmetric(
+ horizontal: 16,
+ vertical: 12,
+ ),
+ child: Text(
+ (state.searchResults.isEmpty)
+ ? AppStrings.mostSearched
+ : AppStrings.resultsFound(
+ state.searchResults.length,
+ ),
+ style: theme.typography.textTheme.titleSmall?.copyWith(
+ color: theme.colors.title,
+ ),
+ ),
+ ),
+ ),
+ SliverToBoxAdapter(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: rooms
+ .map(
+ (room) => Padding(
+ padding: const EdgeInsets.symmetric(
+ horizontal: 16,
+ ),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ InkWell(
+ onTap: () {
+ Navigator.pushNamed(
+ context,
+ Routes.roomDetailsScreen,
+ arguments: room.id.toString(),
+ );
+ },
+ child: WorkspaceDetailsCard(
+ title: room.name,
+ imageUrl: room.imageUrls.isNotEmpty
+ ? room.imageUrls[0]
+ : '',
+ rating: room.rate,
+ price:
+ "${room.pricePerHour.toInt().toString()}/h",
+ location: room.locationName,
+ amenities: room.services
+ .map((e) => e.toString())
+ .toList(),
+ ),
+ ),
+ const SizedBox(height: 12),
+ ],
+ ),
+ ),
+ )
+ .toList(),
+ ),
+ ),
+ ],
+ );
+ } else if (state is SearchError) {
+ return const ErrorScreen(hasAppBar: false);
+ }
+ return const SizedBox.shrink();
+ },
+ ),
+ ),
);
}
}
diff --git a/lib/presentation/screen/search/state/search_cubit.dart b/lib/presentation/screen/search/state/search_cubit.dart
new file mode 100644
index 0000000..18e1626
--- /dev/null
+++ b/lib/presentation/screen/search/state/search_cubit.dart
@@ -0,0 +1,270 @@
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:share_space/presentation/screen/shared/ui_state/workspace_ui_state.dart';
+
+import '../../../../domain/entity/search.dart';
+import '../../../../domain/usecase/search/search_workspaces.dart';
+import '../../../../domain/usecase/workspace/get_best.dart';
+import '../../shared/ui_state/mapper/workspace_ui_mapper.dart';
+import 'search_state.dart';
+
+class SearchCubit extends Cubit {
+ final GetBestUseCase _getBestUseCase;
+ final SearchWorkspacesUseCase _searchWorkspacesUseCase;
+
+ SearchCubit(this._getBestUseCase, this._searchWorkspacesUseCase)
+ : super(SearchInitial());
+
+ Future fetchSearch(
+ String keyword,
+ ) async {
+ final currentState = state;
+ if (currentState is SearchLoaded) {
+ try {
+ emit(SearchLoading());
+ final searchResults = await _searchWorkspacesUseCase.execute(
+ SearchRequest(
+ keyword: keyword,
+ minPrice: currentState.priceStart.toDouble(),
+ maxPrice: currentState.priceEnd.toDouble(),
+ rating: currentState.selectedRate.toDouble(),
+ services: currentState.selectedServicesIndices.map((e) => ServicesUiState.values[e].toString()).toList(),
+ // latitude: latitude,
+ // longitude: longitude,
+ // location: location,
+ ),
+ );
+ emit(
+ SearchLoaded(
+ currentState.query,
+ currentState.isSearching,
+ currentState.selectedRate,
+ currentState.priceStart,
+ currentState.priceEnd,
+ currentState.priceStart,
+ currentState.priceEnd,
+ currentState.selectedRateIndices,
+ currentState.selectedServicesIndices,
+ currentState.services,
+ currentState.lastViewed,
+ searchResults.map((e) => mapWorkToUiState(e)).toList(),
+ ),
+ );
+ } catch (e) {
+ emit(SearchError(e.toString()));
+ }
+ }
+ }
+
+ Future fetchLastViewed() async {
+ final currentState = state;
+ emit(SearchLoading());
+ try {
+ final lastViewed = await _getBestUseCase();
+ double priceStart = 20;
+ double priceEnd = 30;
+ if (lastViewed.isNotEmpty) {
+ final prices = lastViewed
+ .map((e) => (e.pricePerHour).toDouble())
+ .toList();
+ priceStart = prices.reduce((a, b) => a < b ? a : b);
+ priceEnd = prices.reduce((a, b) => a > b ? a : b);
+ }
+
+ emit(
+ SearchLoaded(
+ '',
+ false,
+ 0,
+ (priceStart * 0.5).toInt(),
+ (priceEnd * 1.5).toInt(),
+ priceStart.toInt(),
+ priceEnd.toInt(),
+ [],
+ [],
+ [],
+ lastViewed.map((e) => mapWorkToUiState(e)).toList(),
+ [],
+ ),
+ );
+ } catch (e) {
+ emit(SearchError(e.toString()));
+ }
+ }
+
+ void updateQuery(String query) {
+ final currentState = state;
+ if (currentState is SearchLoaded) {
+ emit(
+ SearchLoaded(
+ query,
+ currentState.isSearching,
+ currentState.selectedRate,
+ currentState.priceStartRange,
+ currentState.priceEndRange,
+ currentState.priceStart,
+ currentState.priceEnd,
+ currentState.selectedRateIndices,
+ currentState.selectedServicesIndices,
+ currentState.services,
+ currentState.lastViewed,
+ currentState.searchResults,
+ ),
+ );
+ }
+ fetchSearch(query);
+ }
+
+ void updateIsSearching(bool isSearching) {
+ final currentState = state;
+ if (currentState is SearchLoaded) {
+ emit(
+ SearchLoaded(
+ currentState.query,
+ isSearching,
+ currentState.selectedRate,
+ currentState.priceStartRange,
+ currentState.priceEndRange,
+ currentState.priceStart,
+ currentState.priceEnd,
+ currentState.selectedRateIndices,
+ currentState.selectedServicesIndices,
+ currentState.services,
+ currentState.lastViewed,
+ currentState.searchResults,
+ ),
+ );
+ }
+ }
+
+ void updateSelectedRate(double selectedRate) {
+ final currentState = state;
+ if (currentState is SearchLoaded) {
+ emit(
+ SearchLoaded(
+ currentState.query,
+ currentState.isSearching,
+ selectedRate.toInt(),
+ currentState.priceStartRange,
+ currentState.priceEndRange,
+ currentState.priceStart,
+ currentState.priceEnd,
+ currentState.selectedRateIndices,
+ currentState.selectedServicesIndices,
+ currentState.services,
+ currentState.lastViewed,
+ currentState.searchResults,
+ ),
+ );
+ }
+ }
+
+ void updatePriceStart(int priceStart) {
+ final currentState = state;
+ if (currentState is SearchLoaded) {
+ emit(
+ SearchLoaded(
+ currentState.query,
+ currentState.isSearching,
+ currentState.selectedRate,
+ currentState.priceStartRange,
+ currentState.priceEndRange,
+ priceStart,
+ currentState.priceEnd,
+ currentState.selectedRateIndices,
+ currentState.selectedServicesIndices,
+ currentState.services,
+ currentState.lastViewed,
+ currentState.searchResults,
+ ),
+ );
+ }
+ }
+
+ void updatePriceEnd(int priceEnd) {
+ final currentState = state;
+ if (currentState is SearchLoaded) {
+ emit(
+ SearchLoaded(
+ currentState.query,
+ currentState.isSearching,
+ currentState.selectedRate,
+ currentState.priceStartRange,
+ currentState.priceEndRange,
+ currentState.priceStart,
+ priceEnd,
+ currentState.selectedRateIndices,
+ currentState.selectedServicesIndices,
+ currentState.services,
+ currentState.lastViewed,
+ currentState.searchResults,
+ ),
+ );
+ }
+ }
+
+ void updateServices(List services) {
+ final currentState = state;
+ if (currentState is SearchLoaded) {
+ emit(
+ SearchLoaded(
+ currentState.query,
+ currentState.isSearching,
+ currentState.selectedRate,
+ currentState.priceStartRange,
+ currentState.priceEndRange,
+ currentState.priceStart,
+ currentState.priceEnd,
+ currentState.selectedRateIndices,
+ currentState.selectedServicesIndices,
+ services,
+ currentState.lastViewed,
+ currentState.searchResults,
+ ),
+ );
+ }
+ }
+
+ void updateSelectedServices(List services) {
+ final currentState = state;
+ if (currentState is SearchLoaded) {
+ emit(
+ SearchLoaded(
+ currentState.query,
+ currentState.isSearching,
+ currentState.selectedRate,
+ currentState.priceStartRange,
+ currentState.priceEndRange,
+ currentState.priceStart,
+ currentState.priceEnd,
+ currentState.selectedRateIndices,
+ services,
+ currentState.services,
+ currentState.lastViewed,
+ currentState.searchResults,
+ ),
+ );
+ }
+ }
+
+ void updateSelectedRates(List rates) {
+ final currentState = state;
+ if (currentState is SearchLoaded) {
+ emit(
+ SearchLoaded(
+ currentState.query,
+ currentState.isSearching,
+ currentState.selectedRate,
+ currentState.priceStartRange,
+ currentState.priceEndRange,
+ currentState.priceStart,
+ currentState.priceEnd,
+ rates,
+ currentState.selectedServicesIndices,
+ currentState.services,
+ currentState.lastViewed,
+ currentState.searchResults,
+ ),
+ );
+ }
+ }
+}
diff --git a/lib/presentation/screen/search/state/search_state.dart b/lib/presentation/screen/search/state/search_state.dart
new file mode 100644
index 0000000..0d22310
--- /dev/null
+++ b/lib/presentation/screen/search/state/search_state.dart
@@ -0,0 +1,43 @@
+import 'package:share_space/presentation/screen/shared/ui_state/workspace_ui_state.dart';
+
+abstract class SearchState {}
+
+class SearchInitial extends SearchState {}
+
+class SearchLoading extends SearchState {}
+
+class SearchLoaded extends SearchState {
+ final String query;
+ final bool isSearching;
+ final int selectedRate;
+ final int priceStartRange;
+ final int priceEndRange;
+ final int priceStart;
+ final int priceEnd;
+ final List selectedRateIndices;
+ final List selectedServicesIndices;
+ final List services;
+ final List lastViewed;
+ final List searchResults;
+
+ SearchLoaded(
+ this.query,
+ this.isSearching,
+ this.selectedRate,
+ this.priceStartRange,
+ this.priceEndRange,
+ this.priceStart,
+ this.priceEnd,
+ this.selectedRateIndices,
+ this.selectedServicesIndices,
+ this.services,
+ this.lastViewed,
+ this.searchResults,
+ );
+}
+
+class SearchError extends SearchState {
+ final String message;
+
+ SearchError(this.message);
+}
diff --git a/lib/presentation/screen/search/widget/custom_chip.dart b/lib/presentation/screen/search/widget/custom_chip.dart
new file mode 100644
index 0000000..9e4ab72
--- /dev/null
+++ b/lib/presentation/screen/search/widget/custom_chip.dart
@@ -0,0 +1,72 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_svg/flutter_svg.dart';
+import 'package:share_space/presentation/design_system/theme/app_theme.dart';
+
+class CustomChip extends StatefulWidget {
+ final String label;
+ Color labelColor;
+ final String icon;
+ bool isSelected;
+ final VoidCallback? onSelect;
+
+ CustomChip({
+ super.key,
+ required this.label,
+ this.labelColor = const Color(0x991F1F1F),
+ this.icon = '',
+ this.isSelected = false,
+ this.onSelect,
+ });
+
+ @override
+ State createState() => _CustomChipState();
+}
+
+class _CustomChipState extends State {
+ @override
+ Widget build(BuildContext context) {
+ final theme = AppTheme.of(context);
+ return GestureDetector(
+ onTap: () => setState(() {
+ widget.onSelect?.call();
+ }),
+ child: Container(
+ padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
+ decoration: BoxDecoration(
+ color: widget.isSelected
+ ? theme.colors.blueVariant
+ : theme.colors.surface,
+ border: Border.all(
+ color: widget.isSelected
+ ? theme.colors.primary
+ : theme.colors.stroke,
+ width: 0.5,
+ ),
+ borderRadius: BorderRadius.circular(20),
+ ),
+ child: Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ if (widget.icon.isNotEmpty) ...[
+ SvgPicture.asset(
+ widget.icon,
+ colorFilter: widget.isSelected
+ ? ColorFilter.mode(theme.colors.primary, BlendMode.srcIn)
+ : null,
+ width: 16,
+ height: 16,
+ ),
+ const SizedBox(width: 8),
+ ],
+ Text(
+ widget.label,
+ style: theme.typography.textTheme.labelSmall?.copyWith(
+ color: widget.isSelected ? theme.colors.primary : widget.labelColor,
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+}
diff --git a/lib/presentation/screen/search/widget/filter_button.dart b/lib/presentation/screen/search/widget/filter_button.dart
new file mode 100644
index 0000000..0ddd42a
--- /dev/null
+++ b/lib/presentation/screen/search/widget/filter_button.dart
@@ -0,0 +1,83 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_svg/flutter_svg.dart';
+import 'package:share_space/presentation/design_system/theme/app_theme.dart';
+
+import '../../../design_system/widget/base_bottom_sheet.dart';
+import '../../shared/ui_state/workspace_ui_state.dart';
+import '../filter_bottomsheet.dart';
+
+class FilterButton extends StatelessWidget {
+ final RangeValues initialRange;
+ RangeValues currentRange;
+ final List rateOptions;
+ final int rateSelectedIndex;
+ List selectedRateIndices;
+ final List servicesOptions;
+ List selectedServicesIndices;
+
+ FilterButton({super.key,
+ this.initialRange = const RangeValues(10, 500),
+ required this.currentRange,
+ required this.rateSelectedIndex,
+ this.rateOptions = const ["All", "1", "2", "3", "4", "5"],
+ required this.selectedRateIndices,
+ this.servicesOptions = ServicesUiState.values,
+ required this.selectedServicesIndices,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ final theme = AppTheme.of(context);
+ return ElevatedButton(
+ style: ElevatedButton.styleFrom(
+ backgroundColor: Colors.transparent,
+ shadowColor: Colors.transparent,
+ shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(100)),
+ padding: const EdgeInsets.all(0),
+ minimumSize: const Size(48, 48),
+ elevation: 0,
+ ),
+ onPressed: () {
+ showBaseBottomSheet(
+ context: context,
+ child: FilterBottomSheet(
+ initialRange: initialRange,
+ currentRange: currentRange,
+ rateSelectedIndex: rateSelectedIndex,
+ rateOptions: rateOptions,
+ selectedRateIndices: selectedRateIndices,
+ servicesOptions: servicesOptions,
+ selectedServicesIndices: selectedServicesIndices,
+ ),
+ label: "Filter",
+ onClose: () {},
+ height: 0.65,
+ );
+ },
+ child: Ink(
+ decoration: BoxDecoration(
+ gradient: const LinearGradient(
+ begin: Alignment.topLeft,
+ end: Alignment.bottomRight,
+ colors: [Color(0xFF50B5E7), Color(0xFF19C6F9)],
+ ),
+ borderRadius: BorderRadius.circular(100),
+ ),
+ child: Container(
+ padding: const EdgeInsets.all(14),
+ constraints: const BoxConstraints(minWidth: 48, minHeight: 48),
+ alignment: Alignment.center,
+ child: SvgPicture.asset(
+ 'assets/icons/filter.svg',
+ width: 24,
+ height: 24,
+ colorFilter: ColorFilter.mode(
+ theme.colors.surfaceLow,
+ BlendMode.srcIn,
+ ),
+ ),
+ ),
+ ),
+ );
+ }
+}
diff --git a/lib/presentation/screen/search/widget/range_chip.dart b/lib/presentation/screen/search/widget/range_chip.dart
new file mode 100644
index 0000000..8a250b0
--- /dev/null
+++ b/lib/presentation/screen/search/widget/range_chip.dart
@@ -0,0 +1,69 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_svg/flutter_svg.dart';
+import '../../../design_system/theme/app_theme.dart';
+
+class RangeChip extends StatefulWidget {
+ final String label;
+ final int value;
+ final String image;
+ final String unit;
+
+ const RangeChip({
+ super.key,
+ required this.label,
+ required this.value,
+ required this.image,
+ required this.unit,
+ });
+
+ @override
+ State createState() => _RangeChipState();
+}
+
+class _RangeChipState extends State {
+ @override
+ Widget build(BuildContext context) {
+ final theme = AppTheme.of(context);
+ return Chip(
+ backgroundColor: theme.colors.surface,
+ shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(100.0)),
+ label: Row(
+ mainAxisSize: MainAxisSize.max,
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ SvgPicture.asset(widget.image, width: 24, height: 24),
+
+ Expanded(
+ flex: 1,
+ child: Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 4),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ widget.label,
+ style: theme.typography.textTheme.labelSmall?.copyWith(
+ color: theme.colors.body,
+ ),
+ ),
+ Text(
+ widget.value.toString(),
+ style: theme.typography.textTheme.labelLarge?.copyWith(
+ color: theme.colors.body,
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ Text(
+ widget.unit,
+ style: theme.typography.textTheme.labelSmall?.copyWith(
+ color: theme.colors.primary,
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/lib/presentation/screen/search/widget/range_slider.dart b/lib/presentation/screen/search/widget/range_slider.dart
new file mode 100644
index 0000000..2476497
--- /dev/null
+++ b/lib/presentation/screen/search/widget/range_slider.dart
@@ -0,0 +1,67 @@
+import 'package:flutter/material.dart';
+
+import '../../../design_system/theme/app_theme.dart';
+
+class RangeSliderWidget extends StatefulWidget {
+ final RangeValues maxRange;
+ RangeValues? currentRange;
+ final String currency;
+ final Function(RangeValues) onChange;
+
+ RangeSliderWidget({
+ super.key,
+ required this.maxRange,
+ required this.currency,
+ this.currentRange,
+ required this.onChange,
+ });
+
+ @override
+ State createState() => _RangeSliderWidgetState();
+}
+
+class _RangeSliderWidgetState extends State {
+ @override
+ Widget build(BuildContext context) {
+ final theme = AppTheme.of(context);
+ return Column(
+ children: [
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Text(
+ '${widget.maxRange.start.round()} ${widget.currency}',
+ style: theme.typography.textTheme.labelSmall?.copyWith(
+ color: theme.colors.body,
+ ),
+ ),
+ Text(
+ '${widget.maxRange.end.round()} ${widget.currency}',
+ style: theme.typography.textTheme.labelSmall?.copyWith(
+ color: theme.colors.body,
+ ),
+ ),
+ ],
+ ),
+ SliderTheme(
+ data: SliderTheme.of(context).copyWith(
+ activeTrackColor: theme.colors.secondary,
+ thumbColor: theme.colors.secondary,
+ overlayShape: RoundSliderOverlayShape(overlayRadius: 20),
+ overlappingShapeStrokeColor: theme.colors.stroke,
+ inactiveTrackColor: theme.colors.surface,
+ trackHeight: 8,
+ overlayColor: Colors.transparent,
+ padding: const EdgeInsets.symmetric(horizontal: 0),
+ ),
+ child: RangeSlider(
+ values: widget.currentRange ?? widget.maxRange,
+ min: widget.maxRange.start,
+ max: widget.maxRange.end,
+ onChanged: widget.onChange,
+ ),
+ ),
+ ],
+ );
+ }
+}
diff --git a/lib/presentation/screen/search/widget/rating_filter.dart b/lib/presentation/screen/search/widget/rating_filter.dart
new file mode 100644
index 0000000..52d5b69
--- /dev/null
+++ b/lib/presentation/screen/search/widget/rating_filter.dart
@@ -0,0 +1,45 @@
+import 'package:flutter/material.dart';
+
+import '../../../design_system/theme/app_theme.dart';
+import 'custom_chip.dart';
+
+class RatingFilter extends StatefulWidget {
+ List options;
+ List selectedIndices;
+ final Function(int) onSelect;
+
+ RatingFilter({
+ super.key,
+ required this.options,
+ required this.selectedIndices,
+ required this.onSelect,
+ });
+
+ @override
+ State createState() => _RatingFilterState();
+}
+
+class _RatingFilterState extends State {
+ @override
+ Widget build(BuildContext context) {
+ final theme = AppTheme.of(context);
+ return Row(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ for (var index = 0; index < widget.options.length; index++)
+ Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 4.0),
+ child: CustomChip(
+ label: widget.options[index],
+ icon: (index != 0) ? "assets/icons/star.svg" : "",
+ labelColor: theme.colors.yellow,
+ isSelected: widget.selectedIndices.contains(index),
+ onSelect: () {
+ widget.onSelect(index);
+ },
+ ),
+ ),
+ ],
+ );
+ }
+}
diff --git a/lib/presentation/screen/search/widget/search_field.dart b/lib/presentation/screen/search/widget/search_field.dart
new file mode 100644
index 0000000..558e943
--- /dev/null
+++ b/lib/presentation/screen/search/widget/search_field.dart
@@ -0,0 +1,129 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_svg/flutter_svg.dart';
+import 'package:share_space/presentation/design_system/theme/app_theme.dart';
+import 'package:share_space/presentation/screen/search/widget/filter_button.dart';
+
+import '../../shared/ui_state/workspace_ui_state.dart';
+
+class SearchField extends StatefulWidget {
+ final ValueChanged onQueryChanged;
+ final List suggestions;
+ final List history;
+ final RangeValues initialRange;
+ RangeValues currentRange;
+ final List rateOptions;
+ final int rateSelectedIndex;
+ List selectedRateIndices;
+ final List servicesOptions;
+ List selectedServicesIndices;
+
+ SearchField({
+ super.key,
+ required this.onQueryChanged,
+ required this.suggestions,
+ required this.history,
+ this.initialRange = const RangeValues(10, 500),
+ required this.currentRange,
+ required this.rateSelectedIndex,
+ this.rateOptions = const ["All", "1", "2", "3", "4", "5"],
+ required this.selectedRateIndices,
+ this.servicesOptions = ServicesUiState.values,
+ required this.selectedServicesIndices,
+ });
+
+ @override
+ State createState() => _SearchFieldState();
+}
+
+class _SearchFieldState extends State {
+ @override
+ Widget build(BuildContext context) {
+ final theme = AppTheme.of(context);
+
+ return Row(
+ children: [
+ Expanded(
+ flex: 1,
+ child: Padding(
+ padding: const EdgeInsets.all(8.0),
+ child: SearchAnchor(
+ viewConstraints: BoxConstraints(
+ maxHeight: MediaQuery.of(context).size.height * 0.3,
+ ),
+ viewBackgroundColor: theme.colors.surfaceLow,
+ dividerColor: Colors.transparent,
+ isFullScreen: false,
+ builder: (BuildContext context, SearchController controller) {
+ return SearchBar(
+ hintText: "Search by room, location...",
+ hintStyle: WidgetStatePropertyAll(
+ theme.typography.textTheme.labelMedium?.copyWith(
+ color: theme.colors.body,
+ ),
+ ),
+ elevation: WidgetStatePropertyAll(0.0),
+ backgroundColor: WidgetStatePropertyAll(
+ theme.colors.surfaceLow,
+ ),
+ controller: controller,
+ shape: WidgetStatePropertyAll(
+ RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(100),
+ side: BorderSide(color: theme.colors.stroke, width: 0.5),
+ ),
+ ),
+ padding: const WidgetStatePropertyAll(
+ EdgeInsets.symmetric(horizontal: 16.0),
+ ),
+ onTap: () {
+ // controller.openView();
+ },
+ onChanged: (value) {
+ // controller.openView();
+ // widget.onQueryChanged(value);
+ },
+ onSubmitted: (value) {
+ // controller.openView();
+ widget.onQueryChanged(value);
+ },
+ leading: SvgPicture.asset(
+ 'assets/icons/search_filled.svg',
+ width: 24,
+ height: 24,
+ colorFilter: ColorFilter.mode(
+ theme.colors.body,
+ BlendMode.srcIn,
+ ),
+ ),
+ );
+ },
+ suggestionsBuilder:
+ (BuildContext context, SearchController controller) {
+ return List.generate(5, (int index) {
+ final String item = 'item $index';
+ return ListTile(
+ title: Text(item),
+ onTap: () {
+ setState(() {
+ controller.closeView(item);
+ });
+ },
+ );
+ });
+ },
+ ),
+ ),
+ ),
+ FilterButton(
+ initialRange: widget.initialRange,
+ currentRange: widget.currentRange,
+ rateSelectedIndex: widget.rateSelectedIndex,
+ rateOptions: widget.rateOptions,
+ selectedRateIndices: widget.selectedRateIndices,
+ servicesOptions: widget.servicesOptions,
+ selectedServicesIndices: widget.selectedServicesIndices,
+ ),
+ ],
+ );
+ }
+}
diff --git a/lib/presentation/screen/search/widget/services_filter.dart b/lib/presentation/screen/search/widget/services_filter.dart
new file mode 100644
index 0000000..540bd27
--- /dev/null
+++ b/lib/presentation/screen/search/widget/services_filter.dart
@@ -0,0 +1,55 @@
+import 'package:flutter/material.dart';
+
+import 'custom_chip.dart';
+
+class ServicesFilter extends StatefulWidget {
+ List options;
+ List selectedIndices;
+ final Function(int) onSelect;
+
+ ServicesFilter({
+ super.key,
+ required this.options,
+ required this.selectedIndices,
+ required this.onSelect,
+ });
+
+ @override
+ State createState() => _ServicesFilterState();
+}
+
+class _ServicesFilterState extends State {
+ @override
+ Widget build(BuildContext context) {
+ return Align(
+ alignment: Alignment.topLeft,
+ child: Wrap(
+ alignment: WrapAlignment.start,
+ spacing: 8.0,
+ runSpacing: 8.0,
+ children: [
+ for (var index = 0; index < widget.options.length; index++)
+ CustomChip(
+ label: widget.options[index],
+ icon: iconPathFor(widget.options[index]),
+ isSelected: widget.selectedIndices.contains(index),
+ onSelect: () {
+ widget.onSelect(index);
+ },
+ ),
+ ],
+ ),
+ );
+ }
+
+ String iconPathFor(String label) {
+ final key = label.trim().toLowerCase();
+ const icons = {
+ 'wifi': 'assets/icons/wifi.svg',
+ 'whiteboard': 'assets/icons/canvas.svg',
+ 'call': 'assets/icons/phone.svg',
+ 'power backup': 'assets/icons/plug.svg',
+ };
+ return icons[key] ?? '';
+ }
+}
diff --git a/lib/resources/app_strings.dart b/lib/resources/app_strings.dart
index e0bed06..d83e895 100644
--- a/lib/resources/app_strings.dart
+++ b/lib/resources/app_strings.dart
@@ -90,7 +90,9 @@ class AppStrings {
static const homeCategoriesTitle = 'Categories';
static const homePopularTitle = 'Popular workspaces';
- static const searchScreenTitle = 'Search Screen';
+ static const searchScreenTitle = 'Search';
+ static const mostSearched = 'Most Searched';
+ static String resultsFound(int count) => 'Results found ($count)';
static const bookingScreenTitle = 'Booking Screen';
static const bookingHistoryScreenTitle = 'Booking History Screen';
static const chatScreenTitle = 'Chat Screen';
diff --git a/pubspec.yaml b/pubspec.yaml
index 2d2a0d4..c6ade16 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -66,6 +66,7 @@ dev_dependencies:
# package. See that file for information about deactivating specific lint
# rules and activating additional ones.
flutter_lints: ^5.0.0
+ flutter_svg: ^2.2.1
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec