From 7586979a95884cc04da276c5cfd01b89bf08828b Mon Sep 17 00:00:00 2001 From: Eduard Ghenea <87914375+Edi013@users.noreply.github.com> Date: Fri, 22 Mar 2024 21:06:17 +0200 Subject: [PATCH] Merge pull request #6 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Item_details view + view model * Merge branch 'master' into feature4/auction * Redirect to ItemDetailsView from ViewItemsView. * Items service + view model implementations * Endpoint consumption - get item by id * Item details page and logic. * Creating auction model, auction-create dto and i_auction_service * Created view and viewmodel for create_auction usecase * Auction form validators and service * Add button to create auction in item details page * adding view model * Sync view with view_model * Fixing auctions_service.dart - create endpoint call * fix(auction): use timestamp instead of datetime across http * Creating service and handeling + implementation for ongoing action if… * Get auction dto creation * Before exit. You need to to do parsing between Either and Option * perf(auction): improve look and feel for get/create auction pages usi… * perf(auction): extract auction details to a dedicated widget/view model * feat(auction): implement get all ongoing auctions flow (view/vm/service) --- lib/app/app.dart | 29 +- lib/app/app.locator.dart | 3 + lib/app/app.router.dart | 248 ++++++++++---- lib/config/api_constants.dart | 5 + lib/models/auctions/auction.dart | 18 + lib/models/auctions/auction.freezed.dart | 227 +++++++++++++ lib/models/auctions/auction.g.dart | 23 ++ lib/models/core/category_converter.dart | 16 + lib/models/core/timestamp_converter.dart | 15 + lib/models/dtos/auction_with_item_dto.dart | 23 ++ .../dtos/auction_with_item_dto.freezed.dart | 316 ++++++++++++++++++ lib/models/dtos/auction_with_item_dto.g.dart | 33 ++ lib/models/dtos/create_auction_dto.dart | 17 + .../dtos/create_auction_dto.freezed.dart | 211 ++++++++++++ lib/models/dtos/create_auction_dto.g.dart | 23 ++ lib/models/dtos/get_auction_dto.dart | 13 + lib/models/dtos/get_item_dto.dart | 12 + lib/models/interfaces/i_auctions_service.dart | 17 + lib/models/interfaces/i_items_service.dart | 3 +- lib/models/items/item.dart | 57 ++-- lib/models/items/item.freezed.dart | 239 +++++++++++++ lib/models/items/item.g.dart | 24 ++ lib/models/validators/auction_validator.dart | 34 ++ lib/services/auctions_service.dart | 174 ++++++++++ lib/services/items_service.dart | 62 ++++ lib/ui/common/app_constants.dart | 2 + .../auction_details/auction_details_view.dart | 115 +++++++ .../auction_details_viewmodel.dart | 48 +++ .../create_auction/create_auction_view.dart | 276 +++++++++++++++ .../create_auction_view.form.dart | 181 ++++++++++ .../create_auction_viewmodel.dart | 86 +++++ .../views/create_item/create_item_view.dart | 3 - lib/ui/views/home/home_view.dart | 52 ++- .../views/item_details/item_details_view.dart | 170 ++++++++++ .../item_details/item_details_viewmodel.dart | 55 +++ lib/ui/views/login/login_view.dart | 62 ++-- lib/ui/views/register/register_view.dart | 75 +++-- .../view_auctions/view_auctions_view.dart | 145 ++++++++ .../view_auctions_viewmodel.dart | 44 +++ lib/ui/views/view_items/view_items_view.dart | 109 +++--- .../view_items/view_items_viewmodel.dart | 17 +- pubspec.yaml | 3 + .../create_auction_viewmodel_test.dart | 11 + .../item_details_viewmodel_test.dart | 11 + 44 files changed, 3073 insertions(+), 234 deletions(-) create mode 100644 lib/models/auctions/auction.dart create mode 100644 lib/models/auctions/auction.freezed.dart create mode 100644 lib/models/auctions/auction.g.dart create mode 100644 lib/models/core/category_converter.dart create mode 100644 lib/models/core/timestamp_converter.dart create mode 100644 lib/models/dtos/auction_with_item_dto.dart create mode 100644 lib/models/dtos/auction_with_item_dto.freezed.dart create mode 100644 lib/models/dtos/auction_with_item_dto.g.dart create mode 100644 lib/models/dtos/create_auction_dto.dart create mode 100644 lib/models/dtos/create_auction_dto.freezed.dart create mode 100644 lib/models/dtos/create_auction_dto.g.dart create mode 100644 lib/models/dtos/get_auction_dto.dart create mode 100644 lib/models/dtos/get_item_dto.dart create mode 100644 lib/models/interfaces/i_auctions_service.dart create mode 100644 lib/models/items/item.freezed.dart create mode 100644 lib/models/items/item.g.dart create mode 100644 lib/models/validators/auction_validator.dart create mode 100644 lib/services/auctions_service.dart create mode 100644 lib/ui/views/auction_details/auction_details_view.dart create mode 100644 lib/ui/views/auction_details/auction_details_viewmodel.dart create mode 100644 lib/ui/views/create_auction/create_auction_view.dart create mode 100644 lib/ui/views/create_auction/create_auction_view.form.dart create mode 100644 lib/ui/views/create_auction/create_auction_viewmodel.dart create mode 100644 lib/ui/views/item_details/item_details_view.dart create mode 100644 lib/ui/views/item_details/item_details_viewmodel.dart create mode 100644 lib/ui/views/view_auctions/view_auctions_view.dart create mode 100644 lib/ui/views/view_auctions/view_auctions_viewmodel.dart create mode 100644 test/viewmodels/create_auction_viewmodel_test.dart create mode 100644 test/viewmodels/item_details_viewmodel_test.dart diff --git a/lib/app/app.dart b/lib/app/app.dart index 9f2aa7d..0889a64 100644 --- a/lib/app/app.dart +++ b/lib/app/app.dart @@ -1,29 +1,31 @@ import 'package:rainbowbid_frontend/models/interfaces/i_auth_service.dart'; -import 'package:rainbowbid_frontend/services/items_service.dart'; - import 'package:rainbowbid_frontend/models/interfaces/i_items_service.dart'; +import 'package:rainbowbid_frontend/services/auth_service.dart'; import 'package:rainbowbid_frontend/services/items_service.dart'; import 'package:rainbowbid_frontend/ui/bottom_sheets/notice/notice_sheet.dart'; import 'package:rainbowbid_frontend/ui/dialogs/info_alert/info_alert_dialog.dart'; +import 'package:rainbowbid_frontend/ui/views/create_auction/create_auction_view.dart'; +import 'package:rainbowbid_frontend/ui/views/create_item/create_item_view.dart'; import 'package:rainbowbid_frontend/ui/views/home/home_view.dart'; +import 'package:rainbowbid_frontend/ui/views/item_details/item_details_view.dart'; +import 'package:rainbowbid_frontend/ui/views/login/login_view.dart'; +import 'package:rainbowbid_frontend/ui/views/register/register_view.dart'; import 'package:rainbowbid_frontend/ui/views/startup/startup_view.dart'; import 'package:rainbowbid_frontend/ui/views/unknown/unknown_view.dart'; import 'package:stacked/stacked_annotations.dart'; import 'package:stacked_services/stacked_services.dart'; -import 'package:rainbowbid_frontend/services/auth_service.dart'; -import 'package:rainbowbid_frontend/ui/views/register/register_view.dart'; -import 'package:rainbowbid_frontend/ui/views/login/login_view.dart'; -import 'package:rainbowbid_frontend/ui/views/view_items/view_items_view.dart'; - -import '../models/interfaces/i_items_service.dart'; -import 'package:rainbowbid_frontend/ui/views/create_item/create_item_view.dart'; +import '../models/interfaces/i_auctions_service.dart'; +import '../services/auctions_service.dart'; // @stacked-import @StackedApp( routes: [ CustomRoute(page: StartupView, initial: true), - CustomRoute(page: HomeView), + CustomRoute( + page: HomeView, + path: '/home', + ), CustomRoute(page: RegisterView, path: '/auth/register'), CustomRoute(page: LoginView, path: '/auth/login'), CustomRoute( @@ -31,8 +33,9 @@ import 'package:rainbowbid_frontend/ui/views/create_item/create_item_view.dart'; path: '/items/create', ), CustomRoute(page: UnknownView, path: '/404'), - CustomRoute(page: ViewItemsView, path: '/items/all'), + CustomRoute(page: ItemDetailsView, path: '/items/:id'), + CustomRoute(page: CreateAuctionView, path: '/items/:itemId/auction'), // @stacked-route /// When none of the above routes match, redirect to UnknownView @@ -50,6 +53,10 @@ import 'package:rainbowbid_frontend/ui/views/create_item/create_item_view.dart'; classType: ItemsService, asType: IItemsService, ), + LazySingleton( + classType: AuctionsService, + asType: IAuctionService, + ), // @stacked-service ], bottomsheets: [ diff --git a/lib/app/app.locator.dart b/lib/app/app.locator.dart index fa9567d..4f07064 100644 --- a/lib/app/app.locator.dart +++ b/lib/app/app.locator.dart @@ -11,8 +11,10 @@ import 'package:stacked_services/src/dialog/dialog_service.dart'; import 'package:stacked_services/src/navigation/router_service.dart'; import 'package:stacked_shared/stacked_shared.dart'; +import '../models/interfaces/i_auctions_service.dart'; import '../models/interfaces/i_auth_service.dart'; import '../models/interfaces/i_items_service.dart'; +import '../services/auctions_service.dart'; import '../services/auth_service.dart'; import '../services/items_service.dart'; import 'app.router.dart'; @@ -34,6 +36,7 @@ Future setupLocator({ locator.registerLazySingleton(() => RouterService()); locator.registerLazySingleton(() => AuthService()); locator.registerLazySingleton(() => ItemsService()); + locator.registerLazySingleton(() => AuctionsService()); if (stackedRouter == null) { throw Exception( 'Stacked is building to use the Router (Navigator 2.0) navigation but no stackedRouter is supplied. Pass the stackedRouter to the setupLocator function in main.dart'); diff --git a/lib/app/app.router.dart b/lib/app/app.router.dart index 3c8dd77..3c0b4cd 100644 --- a/lib/app/app.router.dart +++ b/lib/app/app.router.dart @@ -5,29 +5,30 @@ // ************************************************************************** // ignore_for_file: no_leading_underscores_for_library_prefixes -import 'package:flutter/material.dart' as _i10; -import 'package:stacked/stacked.dart' as _i9; -import 'package:stacked_services/stacked_services.dart' as _i8; +import 'package:flutter/material.dart' as _i11; +import 'package:stacked/stacked.dart' as _i10; +import 'package:stacked_services/stacked_services.dart' as _i9; +import '../ui/views/create_auction/create_auction_view.dart' as _i8; import '../ui/views/create_item/create_item_view.dart' as _i5; import '../ui/views/home/home_view.dart' as _i2; +import '../ui/views/item_details/item_details_view.dart' as _i7; import '../ui/views/login/login_view.dart' as _i4; import '../ui/views/register/register_view.dart' as _i3; import '../ui/views/startup/startup_view.dart' as _i1; import '../ui/views/unknown/unknown_view.dart' as _i6; -import '../ui/views/view_items/view_items_view.dart' as _i7; final stackedRouter = - StackedRouterWeb(navigatorKey: _i8.StackedService.navigatorKey); + StackedRouterWeb(navigatorKey: _i9.StackedService.navigatorKey); -class StackedRouterWeb extends _i9.RootStackRouter { - StackedRouterWeb({_i10.GlobalKey<_i10.NavigatorState>? navigatorKey}) +class StackedRouterWeb extends _i10.RootStackRouter { + StackedRouterWeb({_i11.GlobalKey<_i11.NavigatorState>? navigatorKey}) : super(navigatorKey); @override - final Map pagesMap = { + final Map pagesMap = { StartupViewRoute.name: (routeData) { - return _i9.CustomPage( + return _i10.CustomPage( routeData: routeData, child: const _i1.StartupView(), opaque: true, @@ -35,7 +36,7 @@ class StackedRouterWeb extends _i9.RootStackRouter { ); }, HomeViewRoute.name: (routeData) { - return _i9.CustomPage( + return _i10.CustomPage( routeData: routeData, child: const _i2.HomeView(), opaque: true, @@ -43,7 +44,7 @@ class StackedRouterWeb extends _i9.RootStackRouter { ); }, RegisterViewRoute.name: (routeData) { - return _i9.CustomPage( + return _i10.CustomPage( routeData: routeData, child: const _i3.RegisterView(), opaque: true, @@ -51,7 +52,7 @@ class StackedRouterWeb extends _i9.RootStackRouter { ); }, LoginViewRoute.name: (routeData) { - return _i9.CustomPage( + return _i10.CustomPage( routeData: routeData, child: const _i4.LoginView(), opaque: true, @@ -59,7 +60,7 @@ class StackedRouterWeb extends _i9.RootStackRouter { ); }, CreateItemViewRoute.name: (routeData) { - return _i9.CustomPage( + return _i10.CustomPage( routeData: routeData, child: const _i5.CreateItemView(), opaque: true, @@ -67,17 +68,38 @@ class StackedRouterWeb extends _i9.RootStackRouter { ); }, UnknownViewRoute.name: (routeData) { - return _i9.CustomPage( + return _i10.CustomPage( routeData: routeData, child: const _i6.UnknownView(), opaque: true, barrierDismissible: false, ); }, - ViewItemsViewRoute.name: (routeData) { - return _i9.CustomPage( + ItemDetailsViewRoute.name: (routeData) { + final pathParams = routeData.inheritedPathParams; + final args = routeData.argsAs( + orElse: () => ItemDetailsViewArgs(id: pathParams.getString('id'))); + return _i10.CustomPage( routeData: routeData, - child: const _i7.ViewItemsView(), + child: _i7.ItemDetailsView( + id: args.id, + key: args.key, + ), + opaque: true, + barrierDismissible: false, + ); + }, + CreateAuctionViewRoute.name: (routeData) { + final pathParams = routeData.inheritedPathParams; + final args = routeData.argsAs( + orElse: () => + CreateAuctionViewArgs(itemId: pathParams.getString('itemId'))); + return _i10.CustomPage( + routeData: routeData, + child: _i8.CreateAuctionView( + itemId: args.itemId, + key: args.key, + ), opaque: true, barrierDismissible: false, ); @@ -85,36 +107,40 @@ class StackedRouterWeb extends _i9.RootStackRouter { }; @override - List<_i9.RouteConfig> get routes => [ - _i9.RouteConfig( + List<_i10.RouteConfig> get routes => [ + _i10.RouteConfig( StartupViewRoute.name, path: '/', ), - _i9.RouteConfig( + _i10.RouteConfig( HomeViewRoute.name, - path: '/home-view', + path: '/home', ), - _i9.RouteConfig( + _i10.RouteConfig( RegisterViewRoute.name, path: '/auth/register', ), - _i9.RouteConfig( + _i10.RouteConfig( LoginViewRoute.name, path: '/auth/login', ), - _i9.RouteConfig( + _i10.RouteConfig( CreateItemViewRoute.name, path: '/items/create', ), - _i9.RouteConfig( + _i10.RouteConfig( UnknownViewRoute.name, path: '/404', ), - _i9.RouteConfig( - ViewItemsViewRoute.name, - path: '/items/all', + _i10.RouteConfig( + ItemDetailsViewRoute.name, + path: '/items/:id', + ), + _i10.RouteConfig( + CreateAuctionViewRoute.name, + path: '/items/:itemId/auction', ), - _i9.RouteConfig( + _i10.RouteConfig( '*#redirect', path: '*', redirectTo: '/404', @@ -125,7 +151,7 @@ class StackedRouterWeb extends _i9.RootStackRouter { /// generated route for /// [_i1.StartupView] -class StartupViewRoute extends _i9.PageRouteInfo { +class StartupViewRoute extends _i10.PageRouteInfo { const StartupViewRoute() : super( StartupViewRoute.name, @@ -137,11 +163,11 @@ class StartupViewRoute extends _i9.PageRouteInfo { /// generated route for /// [_i2.HomeView] -class HomeViewRoute extends _i9.PageRouteInfo { +class HomeViewRoute extends _i10.PageRouteInfo { const HomeViewRoute() : super( HomeViewRoute.name, - path: '/home-view', + path: '/home', ); static const String name = 'HomeView'; @@ -149,7 +175,7 @@ class HomeViewRoute extends _i9.PageRouteInfo { /// generated route for /// [_i3.RegisterView] -class RegisterViewRoute extends _i9.PageRouteInfo { +class RegisterViewRoute extends _i10.PageRouteInfo { const RegisterViewRoute() : super( RegisterViewRoute.name, @@ -161,7 +187,7 @@ class RegisterViewRoute extends _i9.PageRouteInfo { /// generated route for /// [_i4.LoginView] -class LoginViewRoute extends _i9.PageRouteInfo { +class LoginViewRoute extends _i10.PageRouteInfo { const LoginViewRoute() : super( LoginViewRoute.name, @@ -173,7 +199,7 @@ class LoginViewRoute extends _i9.PageRouteInfo { /// generated route for /// [_i5.CreateItemView] -class CreateItemViewRoute extends _i9.PageRouteInfo { +class CreateItemViewRoute extends _i10.PageRouteInfo { const CreateItemViewRoute() : super( CreateItemViewRoute.name, @@ -185,7 +211,7 @@ class CreateItemViewRoute extends _i9.PageRouteInfo { /// generated route for /// [_i6.UnknownView] -class UnknownViewRoute extends _i9.PageRouteInfo { +class UnknownViewRoute extends _i10.PageRouteInfo { const UnknownViewRoute() : super( UnknownViewRoute.name, @@ -196,20 +222,78 @@ class UnknownViewRoute extends _i9.PageRouteInfo { } /// generated route for -/// [_i7.ViewItemsView] -class ViewItemsViewRoute extends _i9.PageRouteInfo { - const ViewItemsViewRoute() - : super( - ViewItemsViewRoute.name, - path: '/items/all', +/// [_i7.ItemDetailsView] +class ItemDetailsViewRoute extends _i10.PageRouteInfo { + ItemDetailsViewRoute({ + required String id, + _i11.Key? key, + }) : super( + ItemDetailsViewRoute.name, + path: '/items/:id', + args: ItemDetailsViewArgs( + id: id, + key: key, + ), + rawPathParams: {'id': id}, + ); + + static const String name = 'ItemDetailsView'; +} + +class ItemDetailsViewArgs { + const ItemDetailsViewArgs({ + required this.id, + this.key, + }); + + final String id; + + final _i11.Key? key; + + @override + String toString() { + return 'ItemDetailsViewArgs{id: $id, key: $key}'; + } +} + +/// generated route for +/// [_i8.CreateAuctionView] +class CreateAuctionViewRoute extends _i10.PageRouteInfo { + CreateAuctionViewRoute({ + required String itemId, + _i11.Key? key, + }) : super( + CreateAuctionViewRoute.name, + path: '/items/:itemId/auction', + args: CreateAuctionViewArgs( + itemId: itemId, + key: key, + ), + rawPathParams: {'itemId': itemId}, ); - static const String name = 'ViewItemsView'; + static const String name = 'CreateAuctionView'; +} + +class CreateAuctionViewArgs { + const CreateAuctionViewArgs({ + required this.itemId, + this.key, + }); + + final String itemId; + + final _i11.Key? key; + + @override + String toString() { + return 'CreateAuctionViewArgs{itemId: $itemId, key: $key}'; + } } -extension RouterStateExtension on _i8.RouterService { +extension RouterStateExtension on _i9.RouterService { Future navigateToStartupView( - {void Function(_i9.NavigationFailure)? onFailure}) async { + {void Function(_i10.NavigationFailure)? onFailure}) async { return navigateTo( const StartupViewRoute(), onFailure: onFailure, @@ -217,7 +301,7 @@ extension RouterStateExtension on _i8.RouterService { } Future navigateToHomeView( - {void Function(_i9.NavigationFailure)? onFailure}) async { + {void Function(_i10.NavigationFailure)? onFailure}) async { return navigateTo( const HomeViewRoute(), onFailure: onFailure, @@ -225,7 +309,7 @@ extension RouterStateExtension on _i8.RouterService { } Future navigateToRegisterView( - {void Function(_i9.NavigationFailure)? onFailure}) async { + {void Function(_i10.NavigationFailure)? onFailure}) async { return navigateTo( const RegisterViewRoute(), onFailure: onFailure, @@ -233,7 +317,7 @@ extension RouterStateExtension on _i8.RouterService { } Future navigateToLoginView( - {void Function(_i9.NavigationFailure)? onFailure}) async { + {void Function(_i10.NavigationFailure)? onFailure}) async { return navigateTo( const LoginViewRoute(), onFailure: onFailure, @@ -241,7 +325,7 @@ extension RouterStateExtension on _i8.RouterService { } Future navigateToCreateItemView( - {void Function(_i9.NavigationFailure)? onFailure}) async { + {void Function(_i10.NavigationFailure)? onFailure}) async { return navigateTo( const CreateItemViewRoute(), onFailure: onFailure, @@ -249,23 +333,43 @@ extension RouterStateExtension on _i8.RouterService { } Future navigateToUnknownView( - {void Function(_i9.NavigationFailure)? onFailure}) async { + {void Function(_i10.NavigationFailure)? onFailure}) async { return navigateTo( const UnknownViewRoute(), onFailure: onFailure, ); } - Future navigateToViewItemsView( - {void Function(_i9.NavigationFailure)? onFailure}) async { + Future navigateToItemDetailsView({ + required String id, + _i11.Key? key, + void Function(_i10.NavigationFailure)? onFailure, + }) async { return navigateTo( - const ViewItemsViewRoute(), + ItemDetailsViewRoute( + id: id, + key: key, + ), + onFailure: onFailure, + ); + } + + Future navigateToCreateAuctionView({ + required String itemId, + _i11.Key? key, + void Function(_i10.NavigationFailure)? onFailure, + }) async { + return navigateTo( + CreateAuctionViewRoute( + itemId: itemId, + key: key, + ), onFailure: onFailure, ); } Future replaceWithStartupView( - {void Function(_i9.NavigationFailure)? onFailure}) async { + {void Function(_i10.NavigationFailure)? onFailure}) async { return replaceWith( const StartupViewRoute(), onFailure: onFailure, @@ -273,7 +377,7 @@ extension RouterStateExtension on _i8.RouterService { } Future replaceWithHomeView( - {void Function(_i9.NavigationFailure)? onFailure}) async { + {void Function(_i10.NavigationFailure)? onFailure}) async { return replaceWith( const HomeViewRoute(), onFailure: onFailure, @@ -281,7 +385,7 @@ extension RouterStateExtension on _i8.RouterService { } Future replaceWithRegisterView( - {void Function(_i9.NavigationFailure)? onFailure}) async { + {void Function(_i10.NavigationFailure)? onFailure}) async { return replaceWith( const RegisterViewRoute(), onFailure: onFailure, @@ -289,7 +393,7 @@ extension RouterStateExtension on _i8.RouterService { } Future replaceWithLoginView( - {void Function(_i9.NavigationFailure)? onFailure}) async { + {void Function(_i10.NavigationFailure)? onFailure}) async { return replaceWith( const LoginViewRoute(), onFailure: onFailure, @@ -297,7 +401,7 @@ extension RouterStateExtension on _i8.RouterService { } Future replaceWithCreateItemView( - {void Function(_i9.NavigationFailure)? onFailure}) async { + {void Function(_i10.NavigationFailure)? onFailure}) async { return replaceWith( const CreateItemViewRoute(), onFailure: onFailure, @@ -305,17 +409,37 @@ extension RouterStateExtension on _i8.RouterService { } Future replaceWithUnknownView( - {void Function(_i9.NavigationFailure)? onFailure}) async { + {void Function(_i10.NavigationFailure)? onFailure}) async { return replaceWith( const UnknownViewRoute(), onFailure: onFailure, ); } - Future replaceWithViewItemsView( - {void Function(_i9.NavigationFailure)? onFailure}) async { + Future replaceWithItemDetailsView({ + required String id, + _i11.Key? key, + void Function(_i10.NavigationFailure)? onFailure, + }) async { + return replaceWith( + ItemDetailsViewRoute( + id: id, + key: key, + ), + onFailure: onFailure, + ); + } + + Future replaceWithCreateAuctionView({ + required String itemId, + _i11.Key? key, + void Function(_i10.NavigationFailure)? onFailure, + }) async { return replaceWith( - const ViewItemsViewRoute(), + CreateAuctionViewRoute( + itemId: itemId, + key: key, + ), onFailure: onFailure, ); } diff --git a/lib/config/api_constants.dart b/lib/config/api_constants.dart index 5ec7a60..b2b650c 100644 --- a/lib/config/api_constants.dart +++ b/lib/config/api_constants.dart @@ -7,6 +7,11 @@ abstract class ApiConstants { static const String loginUrl = '/auth/login'; static const String itemsGetAllUrl = '/items/all'; static const String itemsCreateUrl = '/items/create'; + static const String itemsGetItemByIdUrl = '/items/:id'; + static const String itemsGetImageByItemIdUrl = '/items/:id/image'; + static const String auctionsCreateUrl = '/auctions/create'; + static const String auctionsGetByItemIdUrl = '/auctions/:itemId'; + static const String auctionsGetAllUrl = '/auctions/all'; static const String jwtStorage = "jwt.json"; static const String jwtEncodedStorageKey = "jwt"; diff --git a/lib/models/auctions/auction.dart b/lib/models/auctions/auction.dart new file mode 100644 index 0000000..6d539cf --- /dev/null +++ b/lib/models/auctions/auction.dart @@ -0,0 +1,18 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:rainbowbid_frontend/models/core/timestamp_converter.dart'; + +part 'auction.freezed.dart'; +part 'auction.g.dart'; + +@freezed +class Auction with _$Auction { + factory Auction({ + required String id, + @JsonKey(name: 'starting_price') required double startingPrice, + @JsonKey(name: 'end_date') @TimestampConverter() required DateTime endDate, + @JsonKey(name: 'item_id') required String itemId, + }) = _Auction; + + factory Auction.fromJson(Map json) => + _$AuctionFromJson(json); +} diff --git a/lib/models/auctions/auction.freezed.dart b/lib/models/auctions/auction.freezed.dart new file mode 100644 index 0000000..c18bff5 --- /dev/null +++ b/lib/models/auctions/auction.freezed.dart @@ -0,0 +1,227 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'auction.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +Auction _$AuctionFromJson(Map json) { + return _Auction.fromJson(json); +} + +/// @nodoc +mixin _$Auction { + String get id => throw _privateConstructorUsedError; + @JsonKey(name: 'starting_price') + double get startingPrice => throw _privateConstructorUsedError; + @JsonKey(name: 'end_date') + @TimestampConverter() + DateTime get endDate => throw _privateConstructorUsedError; + @JsonKey(name: 'item_id') + String get itemId => throw _privateConstructorUsedError; + + Map toJson() => throw _privateConstructorUsedError; + @JsonKey(ignore: true) + $AuctionCopyWith get copyWith => throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $AuctionCopyWith<$Res> { + factory $AuctionCopyWith(Auction value, $Res Function(Auction) then) = + _$AuctionCopyWithImpl<$Res, Auction>; + @useResult + $Res call( + {String id, + @JsonKey(name: 'starting_price') double startingPrice, + @JsonKey(name: 'end_date') @TimestampConverter() DateTime endDate, + @JsonKey(name: 'item_id') String itemId}); +} + +/// @nodoc +class _$AuctionCopyWithImpl<$Res, $Val extends Auction> + implements $AuctionCopyWith<$Res> { + _$AuctionCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? startingPrice = null, + Object? endDate = null, + Object? itemId = null, + }) { + return _then(_value.copyWith( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String, + startingPrice: null == startingPrice + ? _value.startingPrice + : startingPrice // ignore: cast_nullable_to_non_nullable + as double, + endDate: null == endDate + ? _value.endDate + : endDate // ignore: cast_nullable_to_non_nullable + as DateTime, + itemId: null == itemId + ? _value.itemId + : itemId // ignore: cast_nullable_to_non_nullable + as String, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$AuctionImplCopyWith<$Res> implements $AuctionCopyWith<$Res> { + factory _$$AuctionImplCopyWith( + _$AuctionImpl value, $Res Function(_$AuctionImpl) then) = + __$$AuctionImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {String id, + @JsonKey(name: 'starting_price') double startingPrice, + @JsonKey(name: 'end_date') @TimestampConverter() DateTime endDate, + @JsonKey(name: 'item_id') String itemId}); +} + +/// @nodoc +class __$$AuctionImplCopyWithImpl<$Res> + extends _$AuctionCopyWithImpl<$Res, _$AuctionImpl> + implements _$$AuctionImplCopyWith<$Res> { + __$$AuctionImplCopyWithImpl( + _$AuctionImpl _value, $Res Function(_$AuctionImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? startingPrice = null, + Object? endDate = null, + Object? itemId = null, + }) { + return _then(_$AuctionImpl( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String, + startingPrice: null == startingPrice + ? _value.startingPrice + : startingPrice // ignore: cast_nullable_to_non_nullable + as double, + endDate: null == endDate + ? _value.endDate + : endDate // ignore: cast_nullable_to_non_nullable + as DateTime, + itemId: null == itemId + ? _value.itemId + : itemId // ignore: cast_nullable_to_non_nullable + as String, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$AuctionImpl implements _Auction { + _$AuctionImpl( + {required this.id, + @JsonKey(name: 'starting_price') required this.startingPrice, + @JsonKey(name: 'end_date') @TimestampConverter() required this.endDate, + @JsonKey(name: 'item_id') required this.itemId}); + + factory _$AuctionImpl.fromJson(Map json) => + _$$AuctionImplFromJson(json); + + @override + final String id; + @override + @JsonKey(name: 'starting_price') + final double startingPrice; + @override + @JsonKey(name: 'end_date') + @TimestampConverter() + final DateTime endDate; + @override + @JsonKey(name: 'item_id') + final String itemId; + + @override + String toString() { + return 'Auction(id: $id, startingPrice: $startingPrice, endDate: $endDate, itemId: $itemId)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$AuctionImpl && + (identical(other.id, id) || other.id == id) && + (identical(other.startingPrice, startingPrice) || + other.startingPrice == startingPrice) && + (identical(other.endDate, endDate) || other.endDate == endDate) && + (identical(other.itemId, itemId) || other.itemId == itemId)); + } + + @JsonKey(ignore: true) + @override + int get hashCode => + Object.hash(runtimeType, id, startingPrice, endDate, itemId); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$AuctionImplCopyWith<_$AuctionImpl> get copyWith => + __$$AuctionImplCopyWithImpl<_$AuctionImpl>(this, _$identity); + + @override + Map toJson() { + return _$$AuctionImplToJson( + this, + ); + } +} + +abstract class _Auction implements Auction { + factory _Auction( + {required final String id, + @JsonKey(name: 'starting_price') required final double startingPrice, + @JsonKey(name: 'end_date') + @TimestampConverter() + required final DateTime endDate, + @JsonKey(name: 'item_id') required final String itemId}) = _$AuctionImpl; + + factory _Auction.fromJson(Map json) = _$AuctionImpl.fromJson; + + @override + String get id; + @override + @JsonKey(name: 'starting_price') + double get startingPrice; + @override + @JsonKey(name: 'end_date') + @TimestampConverter() + DateTime get endDate; + @override + @JsonKey(name: 'item_id') + String get itemId; + @override + @JsonKey(ignore: true) + _$$AuctionImplCopyWith<_$AuctionImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/models/auctions/auction.g.dart b/lib/models/auctions/auction.g.dart new file mode 100644 index 0000000..e058d29 --- /dev/null +++ b/lib/models/auctions/auction.g.dart @@ -0,0 +1,23 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'auction.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$AuctionImpl _$$AuctionImplFromJson(Map json) => + _$AuctionImpl( + id: json['id'] as String, + startingPrice: (json['starting_price'] as num).toDouble(), + endDate: const TimestampConverter().fromJson(json['end_date'] as int), + itemId: json['item_id'] as String, + ); + +Map _$$AuctionImplToJson(_$AuctionImpl instance) => + { + 'id': instance.id, + 'starting_price': instance.startingPrice, + 'end_date': const TimestampConverter().toJson(instance.endDate), + 'item_id': instance.itemId, + }; diff --git a/lib/models/core/category_converter.dart b/lib/models/core/category_converter.dart new file mode 100644 index 0000000..3748d96 --- /dev/null +++ b/lib/models/core/category_converter.dart @@ -0,0 +1,16 @@ +import 'package:json_annotation/json_annotation.dart'; +import 'package:rainbowbid_frontend/models/items/item.dart'; + +class CategoryConverter implements JsonConverter { + const CategoryConverter(); + + @override + Category fromJson(String json) { + return Category.fromValue(json); + } + + @override + String toJson(Category object) { + return object.value; + } +} diff --git a/lib/models/core/timestamp_converter.dart b/lib/models/core/timestamp_converter.dart new file mode 100644 index 0000000..cb4b6b2 --- /dev/null +++ b/lib/models/core/timestamp_converter.dart @@ -0,0 +1,15 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +class TimestampConverter implements JsonConverter { + const TimestampConverter(); + + @override + DateTime fromJson(int json) { + return DateTime.fromMillisecondsSinceEpoch(json * 1000, isUtc: true); + } + + @override + int toJson(DateTime object) { + return object.millisecondsSinceEpoch; + } +} diff --git a/lib/models/dtos/auction_with_item_dto.dart b/lib/models/dtos/auction_with_item_dto.dart new file mode 100644 index 0000000..c01e856 --- /dev/null +++ b/lib/models/dtos/auction_with_item_dto.dart @@ -0,0 +1,23 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:rainbowbid_frontend/models/core/category_converter.dart'; +import 'package:rainbowbid_frontend/models/items/item.dart'; + +part 'auction_with_item_dto.freezed.dart'; +part 'auction_with_item_dto.g.dart'; + +@freezed +class AuctionWithItemDto with _$AuctionWithItemDto { + const factory AuctionWithItemDto({ + required String id, + @JsonKey(name: 'starting_price') required double startingPrice, + @JsonKey(name: 'end_date') required DateTime endDate, + @JsonKey(name: 'item_id') required String itemId, + required String brief, + required String description, + @CategoryConverter() required Category category, + @JsonKey(name: 'user_id') required String userId, + }) = _AuctionWithItemDto; + + factory AuctionWithItemDto.fromJson(Map json) => + _$AuctionWithItemDtoFromJson(json); +} diff --git a/lib/models/dtos/auction_with_item_dto.freezed.dart b/lib/models/dtos/auction_with_item_dto.freezed.dart new file mode 100644 index 0000000..f2aa3de --- /dev/null +++ b/lib/models/dtos/auction_with_item_dto.freezed.dart @@ -0,0 +1,316 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'auction_with_item_dto.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +AuctionWithItemDto _$AuctionWithItemDtoFromJson(Map json) { + return _AuctionWithItemDto.fromJson(json); +} + +/// @nodoc +mixin _$AuctionWithItemDto { + String get id => throw _privateConstructorUsedError; + @JsonKey(name: 'starting_price') + double get startingPrice => throw _privateConstructorUsedError; + @JsonKey(name: 'end_date') + DateTime get endDate => throw _privateConstructorUsedError; + @JsonKey(name: 'item_id') + String get itemId => throw _privateConstructorUsedError; + String get brief => throw _privateConstructorUsedError; + String get description => throw _privateConstructorUsedError; + @CategoryConverter() + Category get category => throw _privateConstructorUsedError; + @JsonKey(name: 'user_id') + String get userId => throw _privateConstructorUsedError; + + Map toJson() => throw _privateConstructorUsedError; + @JsonKey(ignore: true) + $AuctionWithItemDtoCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $AuctionWithItemDtoCopyWith<$Res> { + factory $AuctionWithItemDtoCopyWith( + AuctionWithItemDto value, $Res Function(AuctionWithItemDto) then) = + _$AuctionWithItemDtoCopyWithImpl<$Res, AuctionWithItemDto>; + @useResult + $Res call( + {String id, + @JsonKey(name: 'starting_price') double startingPrice, + @JsonKey(name: 'end_date') DateTime endDate, + @JsonKey(name: 'item_id') String itemId, + String brief, + String description, + @CategoryConverter() Category category, + @JsonKey(name: 'user_id') String userId}); +} + +/// @nodoc +class _$AuctionWithItemDtoCopyWithImpl<$Res, $Val extends AuctionWithItemDto> + implements $AuctionWithItemDtoCopyWith<$Res> { + _$AuctionWithItemDtoCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? startingPrice = null, + Object? endDate = null, + Object? itemId = null, + Object? brief = null, + Object? description = null, + Object? category = null, + Object? userId = null, + }) { + return _then(_value.copyWith( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String, + startingPrice: null == startingPrice + ? _value.startingPrice + : startingPrice // ignore: cast_nullable_to_non_nullable + as double, + endDate: null == endDate + ? _value.endDate + : endDate // ignore: cast_nullable_to_non_nullable + as DateTime, + itemId: null == itemId + ? _value.itemId + : itemId // ignore: cast_nullable_to_non_nullable + as String, + brief: null == brief + ? _value.brief + : brief // ignore: cast_nullable_to_non_nullable + as String, + description: null == description + ? _value.description + : description // ignore: cast_nullable_to_non_nullable + as String, + category: null == category + ? _value.category + : category // ignore: cast_nullable_to_non_nullable + as Category, + userId: null == userId + ? _value.userId + : userId // ignore: cast_nullable_to_non_nullable + as String, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$AuctionWithItemDtoImplCopyWith<$Res> + implements $AuctionWithItemDtoCopyWith<$Res> { + factory _$$AuctionWithItemDtoImplCopyWith(_$AuctionWithItemDtoImpl value, + $Res Function(_$AuctionWithItemDtoImpl) then) = + __$$AuctionWithItemDtoImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {String id, + @JsonKey(name: 'starting_price') double startingPrice, + @JsonKey(name: 'end_date') DateTime endDate, + @JsonKey(name: 'item_id') String itemId, + String brief, + String description, + @CategoryConverter() Category category, + @JsonKey(name: 'user_id') String userId}); +} + +/// @nodoc +class __$$AuctionWithItemDtoImplCopyWithImpl<$Res> + extends _$AuctionWithItemDtoCopyWithImpl<$Res, _$AuctionWithItemDtoImpl> + implements _$$AuctionWithItemDtoImplCopyWith<$Res> { + __$$AuctionWithItemDtoImplCopyWithImpl(_$AuctionWithItemDtoImpl _value, + $Res Function(_$AuctionWithItemDtoImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? startingPrice = null, + Object? endDate = null, + Object? itemId = null, + Object? brief = null, + Object? description = null, + Object? category = null, + Object? userId = null, + }) { + return _then(_$AuctionWithItemDtoImpl( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String, + startingPrice: null == startingPrice + ? _value.startingPrice + : startingPrice // ignore: cast_nullable_to_non_nullable + as double, + endDate: null == endDate + ? _value.endDate + : endDate // ignore: cast_nullable_to_non_nullable + as DateTime, + itemId: null == itemId + ? _value.itemId + : itemId // ignore: cast_nullable_to_non_nullable + as String, + brief: null == brief + ? _value.brief + : brief // ignore: cast_nullable_to_non_nullable + as String, + description: null == description + ? _value.description + : description // ignore: cast_nullable_to_non_nullable + as String, + category: null == category + ? _value.category + : category // ignore: cast_nullable_to_non_nullable + as Category, + userId: null == userId + ? _value.userId + : userId // ignore: cast_nullable_to_non_nullable + as String, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$AuctionWithItemDtoImpl implements _AuctionWithItemDto { + const _$AuctionWithItemDtoImpl( + {required this.id, + @JsonKey(name: 'starting_price') required this.startingPrice, + @JsonKey(name: 'end_date') required this.endDate, + @JsonKey(name: 'item_id') required this.itemId, + required this.brief, + required this.description, + @CategoryConverter() required this.category, + @JsonKey(name: 'user_id') required this.userId}); + + factory _$AuctionWithItemDtoImpl.fromJson(Map json) => + _$$AuctionWithItemDtoImplFromJson(json); + + @override + final String id; + @override + @JsonKey(name: 'starting_price') + final double startingPrice; + @override + @JsonKey(name: 'end_date') + final DateTime endDate; + @override + @JsonKey(name: 'item_id') + final String itemId; + @override + final String brief; + @override + final String description; + @override + @CategoryConverter() + final Category category; + @override + @JsonKey(name: 'user_id') + final String userId; + + @override + String toString() { + return 'AuctionWithItemDto(id: $id, startingPrice: $startingPrice, endDate: $endDate, itemId: $itemId, brief: $brief, description: $description, category: $category, userId: $userId)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$AuctionWithItemDtoImpl && + (identical(other.id, id) || other.id == id) && + (identical(other.startingPrice, startingPrice) || + other.startingPrice == startingPrice) && + (identical(other.endDate, endDate) || other.endDate == endDate) && + (identical(other.itemId, itemId) || other.itemId == itemId) && + (identical(other.brief, brief) || other.brief == brief) && + (identical(other.description, description) || + other.description == description) && + (identical(other.category, category) || + other.category == category) && + (identical(other.userId, userId) || other.userId == userId)); + } + + @JsonKey(ignore: true) + @override + int get hashCode => Object.hash(runtimeType, id, startingPrice, endDate, + itemId, brief, description, category, userId); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$AuctionWithItemDtoImplCopyWith<_$AuctionWithItemDtoImpl> get copyWith => + __$$AuctionWithItemDtoImplCopyWithImpl<_$AuctionWithItemDtoImpl>( + this, _$identity); + + @override + Map toJson() { + return _$$AuctionWithItemDtoImplToJson( + this, + ); + } +} + +abstract class _AuctionWithItemDto implements AuctionWithItemDto { + const factory _AuctionWithItemDto( + {required final String id, + @JsonKey(name: 'starting_price') required final double startingPrice, + @JsonKey(name: 'end_date') required final DateTime endDate, + @JsonKey(name: 'item_id') required final String itemId, + required final String brief, + required final String description, + @CategoryConverter() required final Category category, + @JsonKey(name: 'user_id') required final String userId}) = + _$AuctionWithItemDtoImpl; + + factory _AuctionWithItemDto.fromJson(Map json) = + _$AuctionWithItemDtoImpl.fromJson; + + @override + String get id; + @override + @JsonKey(name: 'starting_price') + double get startingPrice; + @override + @JsonKey(name: 'end_date') + DateTime get endDate; + @override + @JsonKey(name: 'item_id') + String get itemId; + @override + String get brief; + @override + String get description; + @override + @CategoryConverter() + Category get category; + @override + @JsonKey(name: 'user_id') + String get userId; + @override + @JsonKey(ignore: true) + _$$AuctionWithItemDtoImplCopyWith<_$AuctionWithItemDtoImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/models/dtos/auction_with_item_dto.g.dart b/lib/models/dtos/auction_with_item_dto.g.dart new file mode 100644 index 0000000..f8829e7 --- /dev/null +++ b/lib/models/dtos/auction_with_item_dto.g.dart @@ -0,0 +1,33 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'auction_with_item_dto.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$AuctionWithItemDtoImpl _$$AuctionWithItemDtoImplFromJson( + Map json) => + _$AuctionWithItemDtoImpl( + id: json['id'] as String, + startingPrice: (json['starting_price'] as num).toDouble(), + endDate: DateTime.parse(json['end_date'] as String), + itemId: json['item_id'] as String, + brief: json['brief'] as String, + description: json['description'] as String, + category: const CategoryConverter().fromJson(json['category'] as String), + userId: json['user_id'] as String, + ); + +Map _$$AuctionWithItemDtoImplToJson( + _$AuctionWithItemDtoImpl instance) => + { + 'id': instance.id, + 'starting_price': instance.startingPrice, + 'end_date': instance.endDate.toIso8601String(), + 'item_id': instance.itemId, + 'brief': instance.brief, + 'description': instance.description, + 'category': const CategoryConverter().toJson(instance.category), + 'user_id': instance.userId, + }; diff --git a/lib/models/dtos/create_auction_dto.dart b/lib/models/dtos/create_auction_dto.dart new file mode 100644 index 0000000..34dc6a2 --- /dev/null +++ b/lib/models/dtos/create_auction_dto.dart @@ -0,0 +1,17 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:rainbowbid_frontend/models/core/timestamp_converter.dart'; + +part 'create_auction_dto.freezed.dart'; +part 'create_auction_dto.g.dart'; + +@freezed +class CreateAuctionDto with _$CreateAuctionDto { + const factory CreateAuctionDto({ + @JsonKey(name: 'item_id') required String itemId, + @JsonKey(name: 'starting_price') required double startingPrice, + @JsonKey(name: 'end_date') @TimestampConverter() required DateTime endDate, + }) = _CreateAuctionDto; + + factory CreateAuctionDto.fromJson(Map json) => + _$CreateAuctionDtoFromJson(json); +} diff --git a/lib/models/dtos/create_auction_dto.freezed.dart b/lib/models/dtos/create_auction_dto.freezed.dart new file mode 100644 index 0000000..7623a64 --- /dev/null +++ b/lib/models/dtos/create_auction_dto.freezed.dart @@ -0,0 +1,211 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'create_auction_dto.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +CreateAuctionDto _$CreateAuctionDtoFromJson(Map json) { + return _CreateAuctionDto.fromJson(json); +} + +/// @nodoc +mixin _$CreateAuctionDto { + @JsonKey(name: 'item_id') + String get itemId => throw _privateConstructorUsedError; + @JsonKey(name: 'starting_price') + double get startingPrice => throw _privateConstructorUsedError; + @JsonKey(name: 'end_date') + @TimestampConverter() + DateTime get endDate => throw _privateConstructorUsedError; + + Map toJson() => throw _privateConstructorUsedError; + @JsonKey(ignore: true) + $CreateAuctionDtoCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $CreateAuctionDtoCopyWith<$Res> { + factory $CreateAuctionDtoCopyWith( + CreateAuctionDto value, $Res Function(CreateAuctionDto) then) = + _$CreateAuctionDtoCopyWithImpl<$Res, CreateAuctionDto>; + @useResult + $Res call( + {@JsonKey(name: 'item_id') String itemId, + @JsonKey(name: 'starting_price') double startingPrice, + @JsonKey(name: 'end_date') @TimestampConverter() DateTime endDate}); +} + +/// @nodoc +class _$CreateAuctionDtoCopyWithImpl<$Res, $Val extends CreateAuctionDto> + implements $CreateAuctionDtoCopyWith<$Res> { + _$CreateAuctionDtoCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? itemId = null, + Object? startingPrice = null, + Object? endDate = null, + }) { + return _then(_value.copyWith( + itemId: null == itemId + ? _value.itemId + : itemId // ignore: cast_nullable_to_non_nullable + as String, + startingPrice: null == startingPrice + ? _value.startingPrice + : startingPrice // ignore: cast_nullable_to_non_nullable + as double, + endDate: null == endDate + ? _value.endDate + : endDate // ignore: cast_nullable_to_non_nullable + as DateTime, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$CreateAuctionDtoImplCopyWith<$Res> + implements $CreateAuctionDtoCopyWith<$Res> { + factory _$$CreateAuctionDtoImplCopyWith(_$CreateAuctionDtoImpl value, + $Res Function(_$CreateAuctionDtoImpl) then) = + __$$CreateAuctionDtoImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {@JsonKey(name: 'item_id') String itemId, + @JsonKey(name: 'starting_price') double startingPrice, + @JsonKey(name: 'end_date') @TimestampConverter() DateTime endDate}); +} + +/// @nodoc +class __$$CreateAuctionDtoImplCopyWithImpl<$Res> + extends _$CreateAuctionDtoCopyWithImpl<$Res, _$CreateAuctionDtoImpl> + implements _$$CreateAuctionDtoImplCopyWith<$Res> { + __$$CreateAuctionDtoImplCopyWithImpl(_$CreateAuctionDtoImpl _value, + $Res Function(_$CreateAuctionDtoImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? itemId = null, + Object? startingPrice = null, + Object? endDate = null, + }) { + return _then(_$CreateAuctionDtoImpl( + itemId: null == itemId + ? _value.itemId + : itemId // ignore: cast_nullable_to_non_nullable + as String, + startingPrice: null == startingPrice + ? _value.startingPrice + : startingPrice // ignore: cast_nullable_to_non_nullable + as double, + endDate: null == endDate + ? _value.endDate + : endDate // ignore: cast_nullable_to_non_nullable + as DateTime, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$CreateAuctionDtoImpl implements _CreateAuctionDto { + const _$CreateAuctionDtoImpl( + {@JsonKey(name: 'item_id') required this.itemId, + @JsonKey(name: 'starting_price') required this.startingPrice, + @JsonKey(name: 'end_date') @TimestampConverter() required this.endDate}); + + factory _$CreateAuctionDtoImpl.fromJson(Map json) => + _$$CreateAuctionDtoImplFromJson(json); + + @override + @JsonKey(name: 'item_id') + final String itemId; + @override + @JsonKey(name: 'starting_price') + final double startingPrice; + @override + @JsonKey(name: 'end_date') + @TimestampConverter() + final DateTime endDate; + + @override + String toString() { + return 'CreateAuctionDto(itemId: $itemId, startingPrice: $startingPrice, endDate: $endDate)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$CreateAuctionDtoImpl && + (identical(other.itemId, itemId) || other.itemId == itemId) && + (identical(other.startingPrice, startingPrice) || + other.startingPrice == startingPrice) && + (identical(other.endDate, endDate) || other.endDate == endDate)); + } + + @JsonKey(ignore: true) + @override + int get hashCode => Object.hash(runtimeType, itemId, startingPrice, endDate); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$CreateAuctionDtoImplCopyWith<_$CreateAuctionDtoImpl> get copyWith => + __$$CreateAuctionDtoImplCopyWithImpl<_$CreateAuctionDtoImpl>( + this, _$identity); + + @override + Map toJson() { + return _$$CreateAuctionDtoImplToJson( + this, + ); + } +} + +abstract class _CreateAuctionDto implements CreateAuctionDto { + const factory _CreateAuctionDto( + {@JsonKey(name: 'item_id') required final String itemId, + @JsonKey(name: 'starting_price') required final double startingPrice, + @JsonKey(name: 'end_date') + @TimestampConverter() + required final DateTime endDate}) = _$CreateAuctionDtoImpl; + + factory _CreateAuctionDto.fromJson(Map json) = + _$CreateAuctionDtoImpl.fromJson; + + @override + @JsonKey(name: 'item_id') + String get itemId; + @override + @JsonKey(name: 'starting_price') + double get startingPrice; + @override + @JsonKey(name: 'end_date') + @TimestampConverter() + DateTime get endDate; + @override + @JsonKey(ignore: true) + _$$CreateAuctionDtoImplCopyWith<_$CreateAuctionDtoImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/models/dtos/create_auction_dto.g.dart b/lib/models/dtos/create_auction_dto.g.dart new file mode 100644 index 0000000..5ef8311 --- /dev/null +++ b/lib/models/dtos/create_auction_dto.g.dart @@ -0,0 +1,23 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'create_auction_dto.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$CreateAuctionDtoImpl _$$CreateAuctionDtoImplFromJson( + Map json) => + _$CreateAuctionDtoImpl( + itemId: json['item_id'] as String, + startingPrice: (json['starting_price'] as num).toDouble(), + endDate: const TimestampConverter().fromJson(json['end_date'] as int), + ); + +Map _$$CreateAuctionDtoImplToJson( + _$CreateAuctionDtoImpl instance) => + { + 'item_id': instance.itemId, + 'starting_price': instance.startingPrice, + 'end_date': const TimestampConverter().toJson(instance.endDate), + }; diff --git a/lib/models/dtos/get_auction_dto.dart b/lib/models/dtos/get_auction_dto.dart new file mode 100644 index 0000000..9fa249b --- /dev/null +++ b/lib/models/dtos/get_auction_dto.dart @@ -0,0 +1,13 @@ +import '../auctions/auction.dart'; +import '../items/item.dart'; + +class GetAuctionDto { + late Auction auction; + + GetAuctionDto({required this.auction}); + + factory GetAuctionDto.fromJson(Map json) { + var auction = Auction.fromJson(json); + return GetAuctionDto(auction: auction); + } +} diff --git a/lib/models/dtos/get_item_dto.dart b/lib/models/dtos/get_item_dto.dart new file mode 100644 index 0000000..ce30ba3 --- /dev/null +++ b/lib/models/dtos/get_item_dto.dart @@ -0,0 +1,12 @@ +import '../items/item.dart'; + +class GetItemDto { + late Item item; + + GetItemDto({required this.item}); + + factory GetItemDto.fromJson(Map json) { + var item = Item.fromJson(json); + return GetItemDto(item: item); + } +} diff --git a/lib/models/interfaces/i_auctions_service.dart b/lib/models/interfaces/i_auctions_service.dart new file mode 100644 index 0000000..ce39de6 --- /dev/null +++ b/lib/models/interfaces/i_auctions_service.dart @@ -0,0 +1,17 @@ +import 'package:dartz/dartz.dart'; +import 'package:rainbowbid_frontend/models/dtos/auction_with_item_dto.dart'; +import 'package:rainbowbid_frontend/models/items/item.dart'; + +import '../auctions/auction.dart'; +import '../dtos/create_auction_dto.dart'; +import '../errors/api_error.dart'; + +abstract interface class IAuctionService { + Future> create({ + required CreateAuctionDto request, + }); + + Future> getAuctionByItemId(String itemId); + + Future>> getAll(Category category); +} diff --git a/lib/models/interfaces/i_items_service.dart b/lib/models/interfaces/i_items_service.dart index 1f9a0a8..5582c7d 100644 --- a/lib/models/interfaces/i_items_service.dart +++ b/lib/models/interfaces/i_items_service.dart @@ -2,6 +2,7 @@ import 'package:dartz/dartz.dart'; import 'package:rainbowbid_frontend/models/dtos/create_item_dto.dart'; import '../dtos/get_all_items_dto.dart'; +import '../dtos/get_item_dto.dart'; import '../errors/api_error.dart'; import '../items/item.dart'; @@ -10,5 +11,5 @@ abstract interface class IItemsService { Future> create({ required CreateItemDto request, }); + Future> getItemById(String id); } - diff --git a/lib/models/items/item.dart b/lib/models/items/item.dart index 735e4d4..33290e2 100644 --- a/lib/models/items/item.dart +++ b/lib/models/items/item.dart @@ -1,35 +1,34 @@ -class Item { - late String id; - late String brief; - late String description; - late String userId; - late Category category; +import 'package:dartz/dartz.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:rainbowbid_frontend/models/core/category_converter.dart'; - Item({ - required this.id, - required this.brief, - required this.description, - required this.userId, - required this.category, - }); +import '../auth/jwt_storage.dart'; - Map toJson() { - return { - 'id': id, - 'brief': brief, - 'description': description, - 'user_id': userId, - 'category': category.value, - }; - } +part 'item.freezed.dart'; +part 'item.g.dart'; + +@freezed +class Item with _$Item { + const factory Item({ + required String id, + required String brief, + required String description, + @JsonKey(name: 'user_id') required String userId, + @CategoryConverter() required Category category, + }) = _Item; + + factory Item.fromJson(Map json) => _$ItemFromJson(json); +} - factory Item.fromJson(Map json) { - return Item( - id: json['id'], - brief: json['brief'], - description: json['description'], - userId: json['user_id'], - category: Category.fromValue(json['category']), +extension ItemX on Item { + Future> getJwtForImageRequest() async { + return (await JwtStorage.getJwt()).fold( + () { + return none(); + }, + (jwt) { + return some(jwt); + }, ); } } diff --git a/lib/models/items/item.freezed.dart b/lib/models/items/item.freezed.dart new file mode 100644 index 0000000..120067e --- /dev/null +++ b/lib/models/items/item.freezed.dart @@ -0,0 +1,239 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'item.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +Item _$ItemFromJson(Map json) { + return _Item.fromJson(json); +} + +/// @nodoc +mixin _$Item { + String get id => throw _privateConstructorUsedError; + String get brief => throw _privateConstructorUsedError; + String get description => throw _privateConstructorUsedError; + @JsonKey(name: 'user_id') + String get userId => throw _privateConstructorUsedError; + @CategoryConverter() + Category get category => throw _privateConstructorUsedError; + + Map toJson() => throw _privateConstructorUsedError; + @JsonKey(ignore: true) + $ItemCopyWith get copyWith => throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $ItemCopyWith<$Res> { + factory $ItemCopyWith(Item value, $Res Function(Item) then) = + _$ItemCopyWithImpl<$Res, Item>; + @useResult + $Res call( + {String id, + String brief, + String description, + @JsonKey(name: 'user_id') String userId, + @CategoryConverter() Category category}); +} + +/// @nodoc +class _$ItemCopyWithImpl<$Res, $Val extends Item> + implements $ItemCopyWith<$Res> { + _$ItemCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? brief = null, + Object? description = null, + Object? userId = null, + Object? category = null, + }) { + return _then(_value.copyWith( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String, + brief: null == brief + ? _value.brief + : brief // ignore: cast_nullable_to_non_nullable + as String, + description: null == description + ? _value.description + : description // ignore: cast_nullable_to_non_nullable + as String, + userId: null == userId + ? _value.userId + : userId // ignore: cast_nullable_to_non_nullable + as String, + category: null == category + ? _value.category + : category // ignore: cast_nullable_to_non_nullable + as Category, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$ItemImplCopyWith<$Res> implements $ItemCopyWith<$Res> { + factory _$$ItemImplCopyWith( + _$ItemImpl value, $Res Function(_$ItemImpl) then) = + __$$ItemImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {String id, + String brief, + String description, + @JsonKey(name: 'user_id') String userId, + @CategoryConverter() Category category}); +} + +/// @nodoc +class __$$ItemImplCopyWithImpl<$Res> + extends _$ItemCopyWithImpl<$Res, _$ItemImpl> + implements _$$ItemImplCopyWith<$Res> { + __$$ItemImplCopyWithImpl(_$ItemImpl _value, $Res Function(_$ItemImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? brief = null, + Object? description = null, + Object? userId = null, + Object? category = null, + }) { + return _then(_$ItemImpl( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String, + brief: null == brief + ? _value.brief + : brief // ignore: cast_nullable_to_non_nullable + as String, + description: null == description + ? _value.description + : description // ignore: cast_nullable_to_non_nullable + as String, + userId: null == userId + ? _value.userId + : userId // ignore: cast_nullable_to_non_nullable + as String, + category: null == category + ? _value.category + : category // ignore: cast_nullable_to_non_nullable + as Category, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$ItemImpl implements _Item { + const _$ItemImpl( + {required this.id, + required this.brief, + required this.description, + @JsonKey(name: 'user_id') required this.userId, + @CategoryConverter() required this.category}); + + factory _$ItemImpl.fromJson(Map json) => + _$$ItemImplFromJson(json); + + @override + final String id; + @override + final String brief; + @override + final String description; + @override + @JsonKey(name: 'user_id') + final String userId; + @override + @CategoryConverter() + final Category category; + + @override + String toString() { + return 'Item(id: $id, brief: $brief, description: $description, userId: $userId, category: $category)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$ItemImpl && + (identical(other.id, id) || other.id == id) && + (identical(other.brief, brief) || other.brief == brief) && + (identical(other.description, description) || + other.description == description) && + (identical(other.userId, userId) || other.userId == userId) && + (identical(other.category, category) || + other.category == category)); + } + + @JsonKey(ignore: true) + @override + int get hashCode => + Object.hash(runtimeType, id, brief, description, userId, category); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$ItemImplCopyWith<_$ItemImpl> get copyWith => + __$$ItemImplCopyWithImpl<_$ItemImpl>(this, _$identity); + + @override + Map toJson() { + return _$$ItemImplToJson( + this, + ); + } +} + +abstract class _Item implements Item { + const factory _Item( + {required final String id, + required final String brief, + required final String description, + @JsonKey(name: 'user_id') required final String userId, + @CategoryConverter() required final Category category}) = _$ItemImpl; + + factory _Item.fromJson(Map json) = _$ItemImpl.fromJson; + + @override + String get id; + @override + String get brief; + @override + String get description; + @override + @JsonKey(name: 'user_id') + String get userId; + @override + @CategoryConverter() + Category get category; + @override + @JsonKey(ignore: true) + _$$ItemImplCopyWith<_$ItemImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/models/items/item.g.dart b/lib/models/items/item.g.dart new file mode 100644 index 0000000..d557d98 --- /dev/null +++ b/lib/models/items/item.g.dart @@ -0,0 +1,24 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'item.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$ItemImpl _$$ItemImplFromJson(Map json) => _$ItemImpl( + id: json['id'] as String, + brief: json['brief'] as String, + description: json['description'] as String, + userId: json['user_id'] as String, + category: const CategoryConverter().fromJson(json['category'] as String), + ); + +Map _$$ItemImplToJson(_$ItemImpl instance) => + { + 'id': instance.id, + 'brief': instance.brief, + 'description': instance.description, + 'user_id': instance.userId, + 'category': const CategoryConverter().toJson(instance.category), + }; diff --git a/lib/models/validators/auction_validator.dart b/lib/models/validators/auction_validator.dart new file mode 100644 index 0000000..5650db3 --- /dev/null +++ b/lib/models/validators/auction_validator.dart @@ -0,0 +1,34 @@ +import 'package:dartz/dartz.dart'; + +import '../../ui/common/app_constants.dart'; + +abstract class AuctionValidator { + static String? validateStartingPrice(String? startingPrice) { + if (startingPrice == null || startingPrice.isEmpty) { + return 'Starting price is required.'; + } + final price = double.tryParse(startingPrice.replaceAll(',', '')); + if (price == null) { + return 'Starting price must be a number.'; + } + if (price < kdMinPrice) { + return 'Starting price must be at least 1.'; + } + + return null; + } + + static String? validateEndDate(Option endDate) { + if (endDate.isNone()) { + return 'End date is required.'; + } + + if (endDate + .fold(() => DateTime.now(), (a) => a) + .isBefore(DateTime.now().add(const Duration(minutes: 1)))) { + return 'End date must be at least 1 minute in the future.'; + } + + return null; + } +} diff --git a/lib/services/auctions_service.dart b/lib/services/auctions_service.dart new file mode 100644 index 0000000..6d3627a --- /dev/null +++ b/lib/services/auctions_service.dart @@ -0,0 +1,174 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:dartz/dartz.dart'; +import 'package:http/browser_client.dart'; +import 'package:rainbowbid_frontend/models/auctions/auction.dart'; +import 'package:rainbowbid_frontend/models/dtos/auction_with_item_dto.dart'; +import 'package:rainbowbid_frontend/models/dtos/create_auction_dto.dart'; +import 'package:rainbowbid_frontend/models/errors/api_error.dart'; +import 'package:rainbowbid_frontend/models/interfaces/i_auctions_service.dart'; +import 'package:rainbowbid_frontend/models/items/item.dart'; + +import '../app/app.logger.dart'; +import '../config/api_constants.dart'; +import '../models/auth/jwt_storage.dart'; + +class AuctionsService implements IAuctionService { + final _logger = getLogger('AuctionsService'); + final _httpClient = BrowserClient()..withCredentials = true; + + @override + Future> create({ + required CreateAuctionDto request, + }) async { + try { + _logger.i("Creating auction for item with id: ${request.itemId}"); + + final accessToken = await JwtStorage.getJwt(); + if (accessToken.isNone()) { + _logger.e("User is not authenticated"); + return left( + const ApiError.unauthorized( + "User is not authenticated", + ), + ); + } + + Map heads = { + HttpHeaders.contentTypeHeader: ContentType.json.mimeType, + HttpHeaders.authorizationHeader: + "Bearer ${accessToken.getOrElse(() => "")}", + }; + + _logger.i(request.toJson()); + final response = await _httpClient.post( + Uri.http( + ApiConstants.baseUrl, + ApiConstants.auctionsCreateUrl, + ), + headers: heads, + body: jsonEncode(request.toJson()), + ); + + switch (response.statusCode) { + case HttpStatus.created: + _logger.i("Auction was successfully created"); + return right(unit); + default: + _logger.e( + "Server error occurred: ${response.statusCode} ${response.body}"); + return left( + ApiError.serverError( + "Server error occurred: ${response.body}", + ), + ); + } + } catch (e) { + _logger.e("Server error occurred: $e"); + return left( + const ApiError.serverError( + "Server error occurred. Please try again later.", + ), + ); + } + } + + @override + Future> getAuctionByItemId(String itemId) async { + try { + _logger.i("Getting auction for item with id: $itemId"); + + Map headers = { + HttpHeaders.contentTypeHeader: ContentType.json.mimeType, + }; + + final response = await _httpClient.get( + Uri.http( + ApiConstants.baseUrl, + ApiConstants.auctionsGetByItemIdUrl.replaceFirst(":itemId", itemId), + ), + headers: headers, + ); + + if (response.statusCode == HttpStatus.ok) { + final jsonBody = jsonDecode(response.body); + final auction = Auction.fromJson(jsonBody); + _logger.i("Auction retrieved successfully: $auction"); + return right(auction); + } else if (response.statusCode == HttpStatus.notFound) { + _logger.e("Auction not found"); + return left( + const ApiError.notFound( + "Auction not found", + ), + ); + } else { + _logger.e( + "Server error occurred: ${response.statusCode} ${response.body}"); + return left( + ApiError.serverError( + "Server error occurred: ${response.body}", + ), + ); + } + } catch (e) { + _logger.e("Server error occurred: $e"); + return left( + const ApiError.serverError( + "Server error occurred. Please try again later.", + ), + ); + } + } + + @override + Future>> getAll( + Category category) async { + try { + _logger.i("Getting all auctions for category: $category"); + + Map headers = { + HttpHeaders.contentTypeHeader: ContentType.json.mimeType, + }; + + final queryParams = {}; + if (category != Category.all) { + queryParams["category"] = category.value; + } + + final response = await _httpClient.get( + Uri.http( + ApiConstants.baseUrl, + ApiConstants.auctionsGetAllUrl, + queryParams, + ), + headers: headers, + ); + + if (response.statusCode == HttpStatus.ok) { + final jsonBody = jsonDecode(response.body); + final List auctions = (jsonBody["auctions"] as List) + .map((auction) => AuctionWithItemDto.fromJson(auction)) + .toList(); + _logger.i("Auctions retrieved successfully: $auctions"); + return right(auctions); + } else { + _logger.e( + "Server error occurred: ${response.statusCode} ${response.body}"); + return left( + ApiError.serverError( + "Server error occurred: ${response.body}", + ), + ); + } + } catch (e) { + _logger.e("Server error occurred: $e"); + return left( + const ApiError.serverError( + "Server error occurred. Please try again later.", + ), + ); + } + } +} diff --git a/lib/services/items_service.dart b/lib/services/items_service.dart index 10b650f..e268a34 100644 --- a/lib/services/items_service.dart +++ b/lib/services/items_service.dart @@ -11,6 +11,7 @@ import 'package:rainbowbid_frontend/models/dtos/create_item_dto.dart'; import 'package:rainbowbid_frontend/models/dtos/get_all_items_dto.dart'; import 'package:rainbowbid_frontend/models/errors/api_error.dart'; import '../app/app.logger.dart'; +import '../models/dtos/get_item_dto.dart'; import '../models/interfaces/i_items_service.dart'; import '../models/items/item.dart'; @@ -86,6 +87,67 @@ class ItemsService implements IItemsService { } } + @override + Future> getItemById(String id) async { + try { + _logger.i("Get item by id."); + final String jwt = (await JwtStorage.getJwt()).fold(() { + return ""; + }, (jwt) { + return jwt; + }); + if (jwt.isEmpty) { + return left( + const ApiError.unauthorized( + "Get item by id was unauthorized.", + ), + ); + } + + Map heads = { + HttpHeaders.contentTypeHeader: ContentType.json.mimeType, + HttpHeaders.authorizationHeader: "Bearer $jwt", + }; + + final response = await _httpClient.get( + Uri.http( + ApiConstants.baseUrl, + ApiConstants.itemsGetItemByIdUrl.replaceFirst(":id", id), + ), + headers: heads, + ); + + switch (response.statusCode) { + case HttpStatus.ok: + _logger.i("Get item was successfully completed"); + return right(GetItemDto.fromJson(json.decode(response.body))); + case HttpStatus.unauthorized: + _logger.i("Get item was unauthorized"); + return left( + const ApiError.unauthorized( + "Get item was unauthorized.", + ), + ); + + default: + _logger.e( + "Server error occurred: ${response.statusCode} ${response.body}"); + return left( + const ApiError.serverError( + "Server error occurred. Please try again later.", + ), + ); + } + } catch (e) { + _logger.e("Server error occurred: $e"); + return left( + const ApiError.serverError( + "Server error occurred. Please try again later.", + ), + ); + } + } + @override Future> create({ required CreateItemDto request, diff --git a/lib/ui/common/app_constants.dart b/lib/ui/common/app_constants.dart index 2609f58..ca2a2cb 100644 --- a/lib/ui/common/app_constants.dart +++ b/lib/ui/common/app_constants.dart @@ -20,6 +20,7 @@ const double kdFormPadding = 20; const String ksRegisterKey = 'registerKey'; const String ksLoginKey = 'loginKey'; const String ksCreateItemKey = 'createItemKey'; +const String ksCreateAuctionKey = 'createAuctionKey'; // validator constants const int kiMinUsernameLength = 3; @@ -32,6 +33,7 @@ const int kiMinItemBriefLength = 3; const int kiMaxItemBriefLength = 30; const int kiMinItemDescriptionLength = 3; const int kiMaxItemDescriptionLength = 255; +const double kdMinPrice = 0.99; // sidebar const int kiSidebarHomeMenuIndex = 0; diff --git a/lib/ui/views/auction_details/auction_details_view.dart b/lib/ui/views/auction_details/auction_details_view.dart new file mode 100644 index 0000000..0ee7a21 --- /dev/null +++ b/lib/ui/views/auction_details/auction_details_view.dart @@ -0,0 +1,115 @@ +import 'package:flutter/material.dart'; +import 'package:rainbowbid_frontend/app/app.router.dart'; +import 'package:rainbowbid_frontend/ui/common/app_colors.dart'; +import 'package:rainbowbid_frontend/ui/common/app_constants.dart'; +import 'package:rainbowbid_frontend/ui/common/ui_helpers.dart'; +import 'package:rainbowbid_frontend/ui/views/auction_details/auction_details_viewmodel.dart'; +import 'package:slide_countdown/slide_countdown.dart'; +import 'package:stacked/stacked.dart'; + +class AuctionDetailsView extends StatelessWidget { + final String itemId; + + const AuctionDetailsView({required this.itemId, super.key}); + + @override + Widget build(BuildContext context) { + return ViewModelBuilder.reactive( + builder: (context, viewModel, child) => viewModel.isBusy + ? const CircularProgressIndicator() + : viewModel.data!.fold( + () { + return ElevatedButton( + onPressed: () async { + await viewModel.routerService.replaceWithCreateAuctionView( + itemId: viewModel.itemId, + ); + }, + style: ElevatedButton.styleFrom( + minimumSize: kButtonSize, + backgroundColor: kcBlue, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(kdFieldBorderRadius), + ), + ), + child: const Text( + "Create auction", + style: TextStyle( + color: kcWhite, + fontSize: kdButtonTextSize, + ), + ), + ); + }, + (auction) { + return Card( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text( + "Ongoing Auction", + style: TextStyle(fontSize: 30, color: kcRed), + ), + verticalSpaceSmall, + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Text( + "Starting price : ${auction.startingPrice}", + style: const TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + const Text( + "Highest bid takes the stake!", + style: TextStyle( + fontSize: 15, + color: kcRed, + fontStyle: FontStyle.italic, + ), + ), + ], + ), + verticalSpaceSmall, + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text( + "Ends in : ", + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + SlideCountdown( + duration: auction.endDate.difference( + DateTime.now(), + ), + slideDirection: SlideDirection.up, + separatorType: SeparatorType.title, + separator: ":", + ), + ], + ), + verticalSpaceMedium, + ], + ), + ), + ], + ), + ), + ); + }, + ), + viewModelBuilder: () => AuctionDetailsViewModel(itemId: itemId), + ); + } +} diff --git a/lib/ui/views/auction_details/auction_details_viewmodel.dart b/lib/ui/views/auction_details/auction_details_viewmodel.dart new file mode 100644 index 0000000..2126747 --- /dev/null +++ b/lib/ui/views/auction_details/auction_details_viewmodel.dart @@ -0,0 +1,48 @@ +import 'package:dartz/dartz.dart'; +import 'package:rainbowbid_frontend/app/app.locator.dart'; +import 'package:rainbowbid_frontend/app/app.logger.dart'; +import 'package:rainbowbid_frontend/models/auctions/auction.dart'; +import 'package:rainbowbid_frontend/models/errors/api_error.dart'; +import 'package:rainbowbid_frontend/models/interfaces/i_auctions_service.dart'; +import 'package:stacked/stacked.dart'; +import 'package:stacked_services/stacked_services.dart'; + +class AuctionDetailsViewModel extends FutureViewModel> { + final _logger = getLogger('AuctionDetailsViewModel'); + + final _routerService = locator(); + final _auctionService = locator(); + + RouterService get routerService => _routerService; + + final String itemId; + + AuctionDetailsViewModel({required this.itemId}); + + @override + Future> futureToRun() => _getAuctionByItemId(itemId); + + Future> _getAuctionByItemId(String itemId) async { + Either result = + await _auctionService.getAuctionByItemId(itemId); + + return result.fold( + (ApiError apiError) { + _logger.e( + "Auction getAuctionByItemId call finished with an error: ${apiError.message}"); + return apiError.maybeWhen( + notFound: (message) { + return none(); + }, + orElse: () { + throw Exception(apiError.message); + }, + ); + }, + (auction) { + _logger.i("Auction getAuctionByItemId call finished."); + return some(auction); + }, + ); + } +} diff --git a/lib/ui/views/create_auction/create_auction_view.dart b/lib/ui/views/create_auction/create_auction_view.dart new file mode 100644 index 0000000..fe3af03 --- /dev/null +++ b/lib/ui/views/create_auction/create_auction_view.dart @@ -0,0 +1,276 @@ +import 'package:dartz/dartz.dart'; +import 'package:flash/flash.dart'; +import 'package:flash/flash_helper.dart'; +import 'package:flutter/material.dart'; +import 'package:omni_datetime_picker/omni_datetime_picker.dart'; +import 'package:pattern_formatter/numeric_formatter.dart'; +import 'package:rainbowbid_frontend/app/app.router.dart'; +import 'package:rainbowbid_frontend/ui/views/create_auction/create_auction_view.form.dart'; +import 'package:rainbowbid_frontend/ui/widgets/app_primitives/app_sidebar.dart'; +import 'package:stacked/stacked.dart'; +import 'package:stacked/stacked_annotations.dart'; + +import '../../../models/auth/jwt_storage.dart'; +import '../../../models/validators/auction_validator.dart'; +import '../../common/app_colors.dart'; +import '../../common/app_constants.dart'; +import '../../common/ui_helpers.dart'; +import 'create_auction_viewmodel.dart'; + +@FormView(fields: [ + FormTextField( + name: 'startingPrice', + validator: AuctionValidator.validateStartingPrice, + ), +]) +class CreateAuctionView extends StackedView + with $CreateAuctionView { + final String itemId; + + const CreateAuctionView({@PathParam() required this.itemId, super.key}); + + @override + Widget builder( + BuildContext context, + CreateAuctionViewModel viewModel, + Widget? child, + ) { + return Scaffold( + body: Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + AppSidebar(controller: viewModel.sidebarController), + Expanded( + child: Center( + child: Padding( + padding: const EdgeInsets.all(kdPagePadding), + child: ScrollConfiguration( + behavior: ScrollConfiguration.of(context).copyWith( + scrollbars: false, + ), + child: SingleChildScrollView( + child: Column( + children: [ + _buildCreateItemPageTitle(context), + verticalSpaceLarge, + _buildStartingPriceField(context, viewModel), + verticalSpaceSmall, + _buildEndDateField(context, viewModel), + verticalSpaceMedium, + _buildCreateAuctionButton(context, viewModel), + ], + ), + ), + ), + ), + ), + ) + ], + ), + ); + } + + Widget _buildCreateItemPageTitle(BuildContext context) { + return const Column( + children: [ + Text( + 'Create auction', + style: TextStyle( + fontSize: kdTitleSize, + fontWeight: FontWeight.bold, + ), + ), + verticalSpaceSmall, + Text( + 'Have the bids flowing now!', + style: TextStyle( + fontSize: kdSubtitleSize, + ), + ), + ], + ); + } + + Widget _buildStartingPriceField( + BuildContext context, + CreateAuctionViewModel viewModel, + ) { + return Column( + children: [ + TextFormField( + controller: startingPriceController, + focusNode: startingPriceFocusNode, + keyboardType: TextInputType.number, + autovalidateMode: AutovalidateMode.onUserInteraction, + decoration: InputDecoration( + floatingLabelBehavior: FloatingLabelBehavior.auto, + focusedBorder: const OutlineInputBorder(), + enabledBorder: const OutlineInputBorder( + borderSide: BorderSide(color: kcLightGrey), + borderRadius: BorderRadius.all( + Radius.circular( + kdFieldBorderRadius, + ), + ), + ), + label: RichText( + text: const TextSpan( + text: 'Starting Price', + style: TextStyle( + fontSize: kdFieldLabelFontSize, + color: kcMediumGrey, + ), + children: [ + TextSpan( + text: ' *', + style: TextStyle( + color: kcRed, + ), + ), + ], + ), + ), + ), + inputFormatters: [ThousandsFormatter(allowFraction: true)], + ), + if (viewModel.hasStartingPriceValidationMessage) ...[ + verticalSpaceTiny, + Text( + viewModel.startingPriceValidationMessage!, + style: const TextStyle( + color: kcRed, + fontSize: kdFieldValidationFontSize, + fontWeight: FontWeight.w700, + ), + ), + ] + ], + ); + } + + Widget _buildEndDateField( + BuildContext context, + CreateAuctionViewModel viewModel, + ) { + return Column( + children: [ + ElevatedButton.icon( + onPressed: () async { + final pickedDate = await showOmniDateTimePicker( + context: context, + ); + + if (pickedDate != null) { + viewModel.endDate = some(pickedDate); + } + }, + icon: const Icon( + Icons.calendar_today, + color: kcWhite, + ), + label: viewModel.endDate.fold( + () => const Text( + 'Select end date', + style: TextStyle( + color: kcWhite, + ), + ), + (endDate) => Text( + 'End date: ${endDate.toIso8601String()}', + style: const TextStyle( + color: kcWhite, + ), + ), + ), + style: ElevatedButton.styleFrom( + minimumSize: kButtonSize, + backgroundColor: kcPrimaryColorDark, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(kdFieldBorderRadius), + ), + ), + ), + if (viewModel.hasEndDateValidationMessage) ...[ + verticalSpaceTiny, + Text( + viewModel.endDateValidationMessage!, + style: const TextStyle( + color: kcRed, + fontSize: kdFieldValidationFontSize, + fontWeight: FontWeight.w700, + ), + ), + ] + ], + ); + } + + Widget _buildCreateAuctionButton( + BuildContext context, + CreateAuctionViewModel viewModel, + ) { + return Column( + children: [ + if (viewModel.busy(ksCreateAuctionKey)) ...[ + const LinearProgressIndicator( + valueColor: AlwaysStoppedAnimation(kcBlue), + ), + verticalSpaceTiny, + ], + ElevatedButton( + onPressed: () async { + await viewModel.createAuction(); + + if (!context.mounted) return; + + if (viewModel.hasErrorForKey(ksCreateAuctionKey)) { + await context.showErrorBar( + position: FlashPosition.top, + indicatorColor: kcRed, + content: Text( + viewModel.error(ksCreateAuctionKey).message as String, + ), + primaryActionBuilder: (context, controller) { + return IconButton( + onPressed: controller.dismiss, + icon: const Icon(Icons.close), + ); + }, + ); + } + }, + style: ElevatedButton.styleFrom( + minimumSize: kButtonSize, + backgroundColor: kcBlue, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(kdFieldBorderRadius), + ), + ), + child: const Text( + 'Create auction', + style: TextStyle( + color: kcWhite, + fontSize: kdButtonTextSize, + ), + ), + ), + ], + ); + } + + @override + CreateAuctionViewModel viewModelBuilder( + BuildContext context, + ) => + CreateAuctionViewModel(itemId: itemId); + + @override + Future onViewModelReady(CreateAuctionViewModel viewModel) async { + final hasCurrentUser = await JwtStorage.hasCurrentUser(); + if (!hasCurrentUser) { + viewModel.routerService.replaceWithLoginView(); + } + + syncFormWithViewModel(viewModel); + } +} diff --git a/lib/ui/views/create_auction/create_auction_view.form.dart b/lib/ui/views/create_auction/create_auction_view.form.dart new file mode 100644 index 0000000..0f0a5d1 --- /dev/null +++ b/lib/ui/views/create_auction/create_auction_view.form.dart @@ -0,0 +1,181 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +// ************************************************************************** +// StackedFormGenerator +// ************************************************************************** + +// ignore_for_file: public_member_api_docs, constant_identifier_names, non_constant_identifier_names,unnecessary_this + +import 'package:flutter/material.dart'; +import 'package:rainbowbid_frontend/models/validators/auction_validator.dart'; +import 'package:stacked/stacked.dart'; + +const bool _autoTextFieldValidation = true; + +const String StartingPriceValueKey = 'startingPrice'; + +final Map + _CreateAuctionViewTextEditingControllers = {}; + +final Map _CreateAuctionViewFocusNodes = {}; + +final Map + _CreateAuctionViewTextValidations = { + StartingPriceValueKey: AuctionValidator.validateStartingPrice, +}; + +mixin $CreateAuctionView { + TextEditingController get startingPriceController => + _getFormTextEditingController(StartingPriceValueKey); + + FocusNode get startingPriceFocusNode => + _getFormFocusNode(StartingPriceValueKey); + + TextEditingController _getFormTextEditingController( + String key, { + String? initialValue, + }) { + if (_CreateAuctionViewTextEditingControllers.containsKey(key)) { + return _CreateAuctionViewTextEditingControllers[key]!; + } + + _CreateAuctionViewTextEditingControllers[key] = + TextEditingController(text: initialValue); + return _CreateAuctionViewTextEditingControllers[key]!; + } + + FocusNode _getFormFocusNode(String key) { + if (_CreateAuctionViewFocusNodes.containsKey(key)) { + return _CreateAuctionViewFocusNodes[key]!; + } + _CreateAuctionViewFocusNodes[key] = FocusNode(); + return _CreateAuctionViewFocusNodes[key]!; + } + + /// Registers a listener on every generated controller that calls [model.setData()] + /// with the latest textController values + void syncFormWithViewModel(FormStateHelper model) { + startingPriceController.addListener(() => _updateFormData(model)); + + _updateFormData(model, forceValidate: _autoTextFieldValidation); + } + + /// Registers a listener on every generated controller that calls [model.setData()] + /// with the latest textController values + @Deprecated( + 'Use syncFormWithViewModel instead.' + 'This feature was deprecated after 3.1.0.', + ) + void listenToFormUpdated(FormViewModel model) { + startingPriceController.addListener(() => _updateFormData(model)); + + _updateFormData(model, forceValidate: _autoTextFieldValidation); + } + + /// Updates the formData on the FormViewModel + void _updateFormData(FormStateHelper model, {bool forceValidate = false}) { + model.setData( + model.formValueMap + ..addAll({ + StartingPriceValueKey: startingPriceController.text, + }), + ); + + if (_autoTextFieldValidation || forceValidate) { + updateValidationData(model); + } + } + + bool validateFormFields(FormViewModel model) { + _updateFormData(model, forceValidate: true); + return model.isFormValid; + } + + /// Calls dispose on all the generated controllers and focus nodes + void disposeForm() { + // The dispose function for a TextEditingController sets all listeners to null + + for (var controller in _CreateAuctionViewTextEditingControllers.values) { + controller.dispose(); + } + for (var focusNode in _CreateAuctionViewFocusNodes.values) { + focusNode.dispose(); + } + + _CreateAuctionViewTextEditingControllers.clear(); + _CreateAuctionViewFocusNodes.clear(); + } +} + +extension ValueProperties on FormStateHelper { + bool get hasAnyValidationMessage => this + .fieldsValidationMessages + .values + .any((validation) => validation != null); + + bool get isFormValid { + if (!_autoTextFieldValidation) this.validateForm(); + + return !hasAnyValidationMessage; + } + + String? get startingPriceValue => + this.formValueMap[StartingPriceValueKey] as String?; + + set startingPriceValue(String? value) { + this.setData( + this.formValueMap..addAll({StartingPriceValueKey: value}), + ); + + if (_CreateAuctionViewTextEditingControllers.containsKey( + StartingPriceValueKey)) { + _CreateAuctionViewTextEditingControllers[StartingPriceValueKey]?.text = + value ?? ''; + } + } + + bool get hasStartingPrice => + this.formValueMap.containsKey(StartingPriceValueKey) && + (startingPriceValue?.isNotEmpty ?? false); + + bool get hasStartingPriceValidationMessage => + this.fieldsValidationMessages[StartingPriceValueKey]?.isNotEmpty ?? false; + + String? get startingPriceValidationMessage => + this.fieldsValidationMessages[StartingPriceValueKey]; +} + +extension Methods on FormStateHelper { + setStartingPriceValidationMessage(String? validationMessage) => + this.fieldsValidationMessages[StartingPriceValueKey] = validationMessage; + + /// Clears text input fields on the Form + void clearForm() { + startingPriceValue = ''; + } + + /// Validates text input fields on the Form + void validateForm() { + this.setValidationMessages({ + StartingPriceValueKey: getValidationMessage(StartingPriceValueKey), + }); + } +} + +/// Returns the validation message for the given key +String? getValidationMessage(String key) { + final validatorForKey = _CreateAuctionViewTextValidations[key]; + if (validatorForKey == null) return null; + + String? validationMessageForKey = validatorForKey( + _CreateAuctionViewTextEditingControllers[key]!.text, + ); + + return validationMessageForKey; +} + +/// Updates the fieldsValidationMessages on the FormViewModel +void updateValidationData(FormStateHelper model) => + model.setValidationMessages({ + StartingPriceValueKey: getValidationMessage(StartingPriceValueKey), + }); diff --git a/lib/ui/views/create_auction/create_auction_viewmodel.dart b/lib/ui/views/create_auction/create_auction_viewmodel.dart new file mode 100644 index 0000000..ad6b9bc --- /dev/null +++ b/lib/ui/views/create_auction/create_auction_viewmodel.dart @@ -0,0 +1,86 @@ +import 'package:dartz/dartz.dart'; +import 'package:rainbowbid_frontend/app/app.router.dart'; +import 'package:rainbowbid_frontend/models/validators/auction_validator.dart'; +import 'package:rainbowbid_frontend/ui/views/create_auction/create_auction_view.form.dart'; +import 'package:sidebarx/sidebarx.dart'; +import 'package:stacked/stacked.dart'; +import 'package:stacked_services/stacked_services.dart'; + +import '../../../app/app.locator.dart'; +import '../../../app/app.logger.dart'; +import '../../../models/dtos/create_auction_dto.dart'; +import '../../../models/errors/api_error.dart'; +import '../../../models/interfaces/i_auctions_service.dart'; +import '../../common/app_constants.dart'; + +class CreateAuctionViewModel extends FormViewModel { + final _logger = getLogger('CreateAuctionViewModel'); + final _auctionService = locator(); + final _routerService = locator(); + final _sidebarController = SidebarXController( + selectedIndex: kiSidebarCreateItemMenuIndex, + ); + final String itemId; + + late Option _endDate = none(); + + RouterService get routerService => _routerService; + SidebarXController get sidebarController => _sidebarController; + + Option get endDate => _endDate; + + set endDate(Option value) { + _endDate = value; + rebuildUi(); + } + + bool get hasEndDateValidationMessage => endDateValidationMessage != null; + + String? get endDateValidationMessage => + AuctionValidator.validateEndDate(endDate); + + CreateAuctionViewModel({required this.itemId}); + + Future createAuction() async { + await runBusyFuture( + _createAuction(), + busyObject: ksCreateAuctionKey, + ); + } + + Future _createAuction() async { + _logger.i('User creates new auction.'); + + await _validate(); + _logger.i('Validation successful'); + + final request = CreateAuctionDto( + itemId: itemId, + startingPrice: double.parse(startingPriceValue!.replaceAll(',', '')), + endDate: endDate.getOrElse(() => DateTime.now()), + ); + + final response = await _auctionService.create(request: request); + + await response.fold( + (ApiError error) { + _logger.e('Error occurred: ${error.message}'); + throw Exception(error.message); + }, + (unit) async { + await _routerService.replaceWithItemDetailsView(id: itemId); + }, + ); + } + + Future _validate() async { + if (hasStartingPriceValidationMessage) { + throw Exception(startingPriceValidationMessage); + } + + final endDateValidationMessage = AuctionValidator.validateEndDate(endDate); + if (endDateValidationMessage != null) { + throw Exception(endDateValidationMessage); + } + } +} diff --git a/lib/ui/views/create_item/create_item_view.dart b/lib/ui/views/create_item/create_item_view.dart index 89c2b5a..561a2a7 100644 --- a/lib/ui/views/create_item/create_item_view.dart +++ b/lib/ui/views/create_item/create_item_view.dart @@ -37,9 +37,6 @@ class CreateItemView extends StackedView ) { return Scaffold( backgroundColor: Theme.of(context).colorScheme.background, - drawer: AppSidebar( - controller: viewModel.sidebarController, - ), body: Row( mainAxisAlignment: MainAxisAlignment.start, children: [ diff --git a/lib/ui/views/home/home_view.dart b/lib/ui/views/home/home_view.dart index 84875a2..14bf175 100644 --- a/lib/ui/views/home/home_view.dart +++ b/lib/ui/views/home/home_view.dart @@ -1,10 +1,11 @@ import 'package:flutter/material.dart'; +import 'package:rainbowbid_frontend/models/auth/jwt_storage.dart'; import 'package:rainbowbid_frontend/ui/common/app_colors.dart'; -import 'package:rainbowbid_frontend/ui/common/app_constants.dart'; import 'package:rainbowbid_frontend/ui/common/ui_helpers.dart'; +import 'package:rainbowbid_frontend/ui/views/home/home_viewmodel.dart'; +import 'package:rainbowbid_frontend/ui/views/view_auctions/view_auctions_view.dart'; import 'package:rainbowbid_frontend/ui/widgets/app_primitives/app_sidebar.dart'; import 'package:stacked/stacked.dart'; -import 'package:rainbowbid_frontend/ui/views/home/home_viewmodel.dart'; import '../view_items/view_items_view.dart'; @@ -25,8 +26,51 @@ class HomeView extends StackedView { controller: viewModel.sidebarController, ), Expanded( - child: Center( - child: ViewItemsView(), + child: Padding( + padding: const EdgeInsets.all(20.0), + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + verticalSpaceSmall, + const Text( + 'Welcome to RainbowBid! 🌈', + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + ), + ), + verticalSpaceLarge, + FutureBuilder( + future: JwtStorage.hasCurrentUser(), + initialData: false, + builder: (context, snapshot) => + snapshot.connectionState == ConnectionState.done + ? Column( + children: [ + if (snapshot.hasData && + snapshot.data! == false) ...[ + const Text( + 'You are not logged in. Please log in to view your items and auctions.', + style: TextStyle( + fontSize: 16, + color: kcRed, + ), + ), + verticalSpaceSmall, + ], + const ViewAuctionsView(), + verticalSpaceLarge, + if (snapshot.hasData && + snapshot.data! == true) + const ViewItemsView(), + ], + ) + : const CircularProgressIndicator(), + ), + ], + ), + ), ), ), ], diff --git a/lib/ui/views/item_details/item_details_view.dart b/lib/ui/views/item_details/item_details_view.dart new file mode 100644 index 0000000..159d329 --- /dev/null +++ b/lib/ui/views/item_details/item_details_view.dart @@ -0,0 +1,170 @@ +import 'dart:io'; + +import 'package:dartz/dartz.dart'; +import 'package:flutter/material.dart'; +import 'package:rainbowbid_frontend/config/api_constants.dart'; +import 'package:rainbowbid_frontend/ui/common/app_colors.dart'; +import 'package:rainbowbid_frontend/ui/common/ui_helpers.dart'; +import 'package:rainbowbid_frontend/ui/views/auction_details/auction_details_view.dart'; +import 'package:rainbowbid_frontend/ui/widgets/app_primitives/app_sidebar.dart'; +import 'package:stacked/stacked.dart'; +import 'package:stacked/stacked_annotations.dart'; + +import '../../../models/items/item.dart'; +import 'item_details_viewmodel.dart'; + +class ItemDetailsView extends StackedView { + final String id; + + const ItemDetailsView({@PathParam() required this.id, super.key}); + + @override + Widget builder( + BuildContext context, + ItemDetailsViewModel viewModel, + Widget? child, + ) { + return Scaffold( + body: viewModel.isBusy + ? const Center(child: CircularProgressIndicator()) + : viewModel.hasError + ? Center( + child: Text( + "Error occurred : ${viewModel.modelError.toString()}", + ), + ) + : Row( + children: [ + AppSidebar(controller: viewModel.sidebarController), + Expanded( + child: Padding( + padding: const EdgeInsets.all(20.0), + child: Column( + children: [ + Card( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + children: [ + Expanded( + child: Column( + mainAxisAlignment: + MainAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: + MainAxisAlignment.start, + children: [ + ClipOval( + child: _buildImageWidget( + viewModel.data!, + ), + ), + horizontalSpaceMedium, + Text( + viewModel.data!.brief, + style: const TextStyle( + fontSize: 30), + ), + horizontalSpaceMedium, + Chip( + label: Text( + viewModel + .data!.category.value, + style: const TextStyle( + fontSize: 25, + fontStyle: FontStyle.italic, + ), + ), + shape: RoundedRectangleBorder( + side: BorderSide( + color: + kcRed.withOpacity(0.5), + ), + borderRadius: + BorderRadius.circular( + 20, + ), + ), + color: MaterialStateColor + .resolveWith( + (states) => + kcRed.withOpacity(0.5), + ), + ), + const Divider(), + ], + ), + verticalSpaceSmall, + Row( + mainAxisAlignment: + MainAxisAlignment.start, + children: [ + Text( + viewModel.data!.description, + style: const TextStyle( + fontSize: 20, + ), + ), + ], + ), + ], + ), + ), + ], + ), + ), + ), + verticalSpaceSmall, + const Divider(), + verticalSpaceSmall, + AuctionDetailsView(itemId: viewModel.data!.id), + ], + ), + ), + ), + ], + ), + ); + } + + Widget _buildImageWidget(Item item) { + return FutureBuilder>( + future: item.getJwtForImageRequest(), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.done) { + if (snapshot.hasData) { + return snapshot.data!.fold(() { + return const SizedBox.shrink(); + }, (jwt) { + Map heads = { + HttpHeaders.contentTypeHeader: ContentType.json.mimeType, + HttpHeaders.authorizationHeader: "Bearer $jwt", + }; + return Image.network( + Uri.http( + ApiConstants.baseUrl, + ApiConstants.itemsGetImageByItemIdUrl.replaceFirst( + ":id", + item.id, + ), + ).toString(), + headers: heads, + ); + }); + } else { + return const SizedBox.shrink(); + } + } else { + return const CircularProgressIndicator(); + } + }, + ); + } + + @override + ItemDetailsViewModel viewModelBuilder( + BuildContext context, + ) => + ItemDetailsViewModel(itemId: id); +} diff --git a/lib/ui/views/item_details/item_details_viewmodel.dart b/lib/ui/views/item_details/item_details_viewmodel.dart new file mode 100644 index 0000000..24b1c29 --- /dev/null +++ b/lib/ui/views/item_details/item_details_viewmodel.dart @@ -0,0 +1,55 @@ +import 'package:dartz/dartz.dart'; +import 'package:rainbowbid_frontend/app/app.router.dart'; +import 'package:sidebarx/sidebarx.dart'; +import 'package:stacked/stacked.dart'; +import 'package:stacked_services/stacked_services.dart'; + +import '../../../app/app.locator.dart'; +import '../../../app/app.logger.dart'; +import '../../../models/auth/jwt_storage.dart'; +import '../../../models/dtos/get_item_dto.dart'; +import '../../../models/errors/api_error.dart'; +import '../../../models/interfaces/i_items_service.dart'; +import '../../../models/items/item.dart'; +import '../../common/app_constants.dart'; + +class ItemDetailsViewModel extends FutureViewModel { + final _logger = getLogger('ItemDetailsViewModel'); + final _sidebarController = SidebarXController( + selectedIndex: kiSidebarCreateItemMenuIndex, + ); + final _routerService = locator(); + final _itemService = locator(); + final String itemId; + + SidebarXController get sidebarController => _sidebarController; + RouterService get routerService => _routerService; + + ItemDetailsViewModel({required this.itemId}); + + @override + Future futureToRun() => _getItemById(); + + Future _getItemById() async { + Either result = + await _itemService.getItemById(itemId); + + return result.fold( + (ApiError apiError) { + _logger.e("Items getById call finished with an error"); + apiError.maybeWhen( + unauthorized: (message) async { + await JwtStorage.clear(); + await _routerService.replaceWithLoginView(); + }, + orElse: () {}, + ); + throw Exception(apiError.message); + }, + (getItemDto) { + _logger.i("Items getById call finished."); + return getItemDto.item; + }, + ); + } +} diff --git a/lib/ui/views/login/login_view.dart b/lib/ui/views/login/login_view.dart index 00becf7..ea96bf3 100644 --- a/lib/ui/views/login/login_view.dart +++ b/lib/ui/views/login/login_view.dart @@ -46,36 +46,38 @@ class LoginView extends StackedView with $LoginView { controller: viewModel.sidebarController, ), Expanded( - child: Center( - child: Padding( - padding: const EdgeInsets.all(kdPagePadding), - child: Card( - child: Padding( - padding: const EdgeInsets.all(kdFormPadding), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - mainAxisSize: MainAxisSize.min, - children: [ - _buildRegisterPageTitle(context), - verticalSpaceLarge, - Column( - children: [ - Row( - children: [ - _buildEmailField(context, viewModel), - ], - ), - verticalSpaceLarge, - Row( - children: [ - _buildPasswordField(context, viewModel), - ], - ), - verticalSpaceMedium, - _buildLoginButton(context, viewModel), - ], - ), - ], + child: SingleChildScrollView( + child: Center( + child: Padding( + padding: const EdgeInsets.all(kdPagePadding), + child: Card( + child: Padding( + padding: const EdgeInsets.all(kdFormPadding), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + _buildRegisterPageTitle(context), + verticalSpaceLarge, + Column( + children: [ + Row( + children: [ + _buildEmailField(context, viewModel), + ], + ), + verticalSpaceLarge, + Row( + children: [ + _buildPasswordField(context, viewModel), + ], + ), + verticalSpaceMedium, + _buildLoginButton(context, viewModel), + ], + ), + ], + ), ), ), ), diff --git a/lib/ui/views/register/register_view.dart b/lib/ui/views/register/register_view.dart index ee3d2f1..00ca2be 100644 --- a/lib/ui/views/register/register_view.dart +++ b/lib/ui/views/register/register_view.dart @@ -52,42 +52,45 @@ class RegisterView extends StackedView with $RegisterView { controller: viewModel.sidebarController, ), Expanded( - child: Center( - child: Padding( - padding: const EdgeInsets.all(kdPagePadding), - child: Card( - child: Padding( - padding: const EdgeInsets.all(kdFormPadding), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - mainAxisSize: MainAxisSize.min, - children: [ - _buildRegisterPageTitle(context), - verticalSpaceLarge, - Column( - children: [ - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _buildNameField(context, viewModel), - horizontalSpaceLarge, - _buildEmailField(context, viewModel), - ], - ), - verticalSpaceLarge, - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _buildPasswordField(context, viewModel), - horizontalSpaceLarge, - _buildConfirmPasswordField(context, viewModel), - ], - ), - verticalSpaceMedium, - _buildRegisterButton(context, viewModel), - ], - ), - ], + child: SingleChildScrollView( + child: Center( + child: Padding( + padding: const EdgeInsets.all(kdPagePadding), + child: Card( + child: Padding( + padding: const EdgeInsets.all(kdFormPadding), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + _buildRegisterPageTitle(context), + verticalSpaceLarge, + Column( + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildNameField(context, viewModel), + horizontalSpaceLarge, + _buildEmailField(context, viewModel), + ], + ), + verticalSpaceLarge, + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildPasswordField(context, viewModel), + horizontalSpaceLarge, + _buildConfirmPasswordField( + context, viewModel), + ], + ), + verticalSpaceMedium, + _buildRegisterButton(context, viewModel), + ], + ), + ], + ), ), ), ), diff --git a/lib/ui/views/view_auctions/view_auctions_view.dart b/lib/ui/views/view_auctions/view_auctions_view.dart new file mode 100644 index 0000000..23486a1 --- /dev/null +++ b/lib/ui/views/view_auctions/view_auctions_view.dart @@ -0,0 +1,145 @@ +import 'package:flutter/material.dart'; +import 'package:rainbowbid_frontend/models/items/item.dart'; +import 'package:rainbowbid_frontend/ui/common/app_colors.dart'; +import 'package:rainbowbid_frontend/ui/common/app_constants.dart'; +import 'package:rainbowbid_frontend/ui/common/ui_helpers.dart'; +import 'package:rainbowbid_frontend/ui/views/view_auctions/view_auctions_viewmodel.dart'; +import 'package:slide_countdown/slide_countdown.dart'; +import 'package:stacked/stacked.dart'; + +class ViewAuctionsView extends StatelessWidget { + const ViewAuctionsView({super.key}); + + @override + Widget build(BuildContext context) { + return ViewModelBuilder.reactive( + builder: (context, viewModel, child) => viewModel.isBusy + ? const CircularProgressIndicator() + : Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Text( + "Ongoing auctions", + style: TextStyle(fontSize: 20), + ), + verticalSpaceMedium, + DropdownButtonFormField( + value: viewModel.selectedCategory, + onChanged: (value) async { + if (value != null) { + viewModel.selectedCategory = value; + await viewModel.refresh(); + } + }, + items: Category.values + .map( + (category) => DropdownMenuItem( + value: category, + child: Text( + category.displayValue, + style: const TextStyle( + fontSize: kdFieldLabelFontSize, + ), + ), + ), + ) + .toList(), + decoration: const InputDecoration( + floatingLabelBehavior: FloatingLabelBehavior.auto, + focusedBorder: OutlineInputBorder(), + enabledBorder: OutlineInputBorder( + borderSide: BorderSide(color: kcLightGrey), + borderRadius: BorderRadius.all( + Radius.circular( + kdFieldBorderRadius, + ), + ), + ), + label: Text( + 'Category', + style: TextStyle( + fontSize: kdFieldLabelFontSize, + color: kcMediumGrey, + ), + ), + ), + ), + verticalSpaceSmall, + ListView.builder( + shrinkWrap: true, + itemCount: viewModel.data!.length, + itemBuilder: (context, index) { + return Card( + child: Column( + children: [ + ListTile( + title: Text( + viewModel.data![index].brief.toString(), + ), + subtitle: Text( + viewModel.data![index].description.toString(), + ), + trailing: Chip( + label: Text( + viewModel.data![index].category.displayValue, + ), + shape: RoundedRectangleBorder( + side: BorderSide( + color: kcRed.withOpacity(0.5), + ), + borderRadius: BorderRadius.circular( + 20, + ), + ), + color: MaterialStateColor.resolveWith( + (states) => kcRed.withOpacity(0.5), + ), + ), + ), + verticalSpaceTiny, + Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text( + "Ends in : ", + style: TextStyle( + fontWeight: FontWeight.bold, + ), + ), + SlideCountdown( + duration: viewModel.data![index].endDate + .difference( + DateTime.now(), + ), + slideDirection: SlideDirection.up, + separatorType: SeparatorType.title, + separator: ":", + ), + ], + ), + horizontalSpaceSmall, + Text( + "Starting price : ${viewModel.data![index].startingPrice}", + style: const TextStyle( + fontWeight: FontWeight.bold, + ), + ), + horizontalSpaceSmall, + ], + ), + ), + ], + ), + ); + }, + ), + ], + ), + viewModelBuilder: () => ViewAuctionsViewModel(), + ); + } +} diff --git a/lib/ui/views/view_auctions/view_auctions_viewmodel.dart b/lib/ui/views/view_auctions/view_auctions_viewmodel.dart new file mode 100644 index 0000000..d174b95 --- /dev/null +++ b/lib/ui/views/view_auctions/view_auctions_viewmodel.dart @@ -0,0 +1,44 @@ +import 'package:rainbowbid_frontend/app/app.locator.dart'; +import 'package:rainbowbid_frontend/app/app.logger.dart'; +import 'package:rainbowbid_frontend/models/dtos/auction_with_item_dto.dart'; +import 'package:rainbowbid_frontend/models/interfaces/i_auctions_service.dart'; +import 'package:rainbowbid_frontend/models/items/item.dart'; +import 'package:stacked/stacked.dart'; +import 'package:stacked_services/stacked_services.dart'; + +class ViewAuctionsViewModel extends FutureViewModel> { + final _logger = getLogger('ViewAuctionsViewModel'); + final _auctionsService = locator(); + final _routerService = locator(); + late Category _selectedCategory = Category.all; + + RouterService get routerService => _routerService; + Category get selectedCategory => _selectedCategory; + + set selectedCategory(Category value) { + _selectedCategory = value; + rebuildUi(); + } + + Future refresh() async { + await initialise(); + } + + Future> _getAll() async { + final result = await _auctionsService.getAll(_selectedCategory); + + return result.fold( + (apiError) { + _logger.e("Auctions getAll call finished with an error"); + return []; + }, + (getAllAuctionsDto) { + _logger.i("Auctions getAll call finished."); + return getAllAuctionsDto; + }, + ); + } + + @override + Future> futureToRun() => _getAll(); +} diff --git a/lib/ui/views/view_items/view_items_view.dart b/lib/ui/views/view_items/view_items_view.dart index dfbb6ed..0e7e0d3 100644 --- a/lib/ui/views/view_items/view_items_view.dart +++ b/lib/ui/views/view_items/view_items_view.dart @@ -1,10 +1,11 @@ import 'package:flutter/material.dart'; +import 'package:rainbowbid_frontend/app/app.router.dart'; +import 'package:rainbowbid_frontend/ui/common/ui_helpers.dart'; import 'package:stacked/stacked.dart'; import '../../../models/items/item.dart'; import '../../common/app_colors.dart'; import '../../common/app_constants.dart'; -import '../../widgets/app_primitives/app_sidebar.dart'; import 'view_items_viewmodel.dart'; class ViewItemsView extends StatelessWidget { @@ -16,51 +17,54 @@ class ViewItemsView extends StatelessWidget { builder: (context, viewModel, child) => viewModel.isBusy ? const CircularProgressIndicator() : Column( - children: [ - const Text("Your items"), - DropdownButtonFormField( - value: viewModel.selectedCategory, - onChanged: (value) async { - if (value != null) { - viewModel.selectedCategory = value; - await viewModel.refresh(); - } - }, - items: Category.values - .map( - (category) => DropdownMenuItem( - value: category, - child: Text( - category.displayValue, - style: const TextStyle( - fontSize: kdFieldLabelFontSize, + mainAxisSize: MainAxisSize.min, + children: [ + const Text("Your items", style: TextStyle(fontSize: 20)), + verticalSpaceMedium, + DropdownButtonFormField( + value: viewModel.selectedCategory, + onChanged: (value) async { + if (value != null) { + viewModel.selectedCategory = value; + await viewModel.refresh(); + } + }, + items: Category.values + .map( + (category) => DropdownMenuItem( + value: category, + child: Text( + category.displayValue, + style: const TextStyle( + fontSize: kdFieldLabelFontSize, + ), ), ), - ), - ) - .toList(), - decoration: const InputDecoration( - floatingLabelBehavior: FloatingLabelBehavior.auto, - focusedBorder: OutlineInputBorder(), - enabledBorder: OutlineInputBorder( - borderSide: BorderSide(color: kcLightGrey), - borderRadius: BorderRadius.all( - Radius.circular( - kdFieldBorderRadius, + ) + .toList(), + decoration: const InputDecoration( + floatingLabelBehavior: FloatingLabelBehavior.auto, + focusedBorder: OutlineInputBorder(), + enabledBorder: OutlineInputBorder( + borderSide: BorderSide(color: kcLightGrey), + borderRadius: BorderRadius.all( + Radius.circular( + kdFieldBorderRadius, + ), ), ), - ), - label: Text( - 'Category', - style: TextStyle( - fontSize: kdFieldLabelFontSize, - color: kcMediumGrey, + label: Text( + 'Category', + style: TextStyle( + fontSize: kdFieldLabelFontSize, + color: kcMediumGrey, + ), ), ), ), - ), - Expanded( - child: ListView.builder( + verticalSpaceSmall, + ListView.builder( + shrinkWrap: true, itemCount: viewModel.data!.length, itemBuilder: (context, index) { return Card( @@ -71,16 +75,33 @@ class ViewItemsView extends StatelessWidget { subtitle: Text( viewModel.data![index].description.toString(), ), - trailing: Text( - viewModel.data![index].category.displayValue, + trailing: Chip( + label: Text( + viewModel.data![index].category.displayValue, + ), + shape: RoundedRectangleBorder( + side: BorderSide( + color: kcRed.withOpacity(0.5), + ), + borderRadius: BorderRadius.circular( + 20, + ), + ), + color: MaterialStateColor.resolveWith( + (states) => kcRed.withOpacity(0.5), + ), ), + onTap: () async { + final item = viewModel.data![index]; + await viewModel.routerService + .replaceWithItemDetailsView(id: item.id); + }, ), ); }, ), - ), - ], - ), + ], + ), viewModelBuilder: () => ViewItemsViewModel(), ); } diff --git a/lib/ui/views/view_items/view_items_viewmodel.dart b/lib/ui/views/view_items/view_items_viewmodel.dart index 3a7535f..c8233fd 100644 --- a/lib/ui/views/view_items/view_items_viewmodel.dart +++ b/lib/ui/views/view_items/view_items_viewmodel.dart @@ -1,7 +1,4 @@ import 'package:dartz/dartz.dart'; -import 'package:rainbowbid_frontend/app/app.router.dart'; -import 'package:rainbowbid_frontend/models/auth/jwt_storage.dart'; -import 'package:sidebarx/sidebarx.dart'; import 'package:stacked/stacked.dart'; import 'package:stacked_services/stacked_services.dart'; @@ -11,7 +8,6 @@ import '../../../models/dtos/get_all_items_dto.dart'; import '../../../models/errors/api_error.dart'; import '../../../models/interfaces/i_items_service.dart'; import '../../../models/items/item.dart'; -import '../../common/app_constants.dart'; class ViewItemsViewModel extends FutureViewModel> { final _logger = getLogger('ViewItemsViewModel'); @@ -19,6 +15,7 @@ class ViewItemsViewModel extends FutureViewModel> { final _routerService = locator(); late Category _selectedCategory = Category.all; + RouterService get routerService => _routerService; Category get selectedCategory => _selectedCategory; set selectedCategory(Category value) { @@ -30,21 +27,13 @@ class ViewItemsViewModel extends FutureViewModel> { await initialise(); } - Future> getAll() async { + Future> _getAll() async { Either result = await _itemsService.getAll(_selectedCategory); return result.fold( (ApiError apiError) { _logger.e("Items getAll call finished with an error"); - apiError.maybeWhen( - unauthorized: (message) async { - await JwtStorage.clear(); - await _routerService.replaceWithLoginView(); - }, - orElse: () {}, - ); - return []; }, (getAllItemsDto) { @@ -67,5 +56,5 @@ class ViewItemsViewModel extends FutureViewModel> { } @override - Future> futureToRun() => getAll(); + Future> futureToRun() => _getAll(); } diff --git a/pubspec.yaml b/pubspec.yaml index 97c3646..2bbb735 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -24,6 +24,9 @@ dependencies: localstorage: ^4.0.1+4 http_parser: ^4.0.2 file_picker: ^6.2.0 + pattern_formatter: ^3.0.0 + omni_datetime_picker: ^1.0.9 + slide_countdown: ^1.5.2 dev_dependencies: build_runner: ^2.4.5 diff --git a/test/viewmodels/create_auction_viewmodel_test.dart b/test/viewmodels/create_auction_viewmodel_test.dart new file mode 100644 index 0000000..7aaef0a --- /dev/null +++ b/test/viewmodels/create_auction_viewmodel_test.dart @@ -0,0 +1,11 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:rainbowbid_frontend/app/app.locator.dart'; + +import '../helpers/test_helpers.dart'; + +void main() { + group('CreateAuctionViewModel Tests -', () { + setUp(() => registerServices()); + tearDown(() => locator.reset()); + }); +} diff --git a/test/viewmodels/item_details_viewmodel_test.dart b/test/viewmodels/item_details_viewmodel_test.dart new file mode 100644 index 0000000..cf4454a --- /dev/null +++ b/test/viewmodels/item_details_viewmodel_test.dart @@ -0,0 +1,11 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:rainbowbid_frontend/app/app.locator.dart'; + +import '../helpers/test_helpers.dart'; + +void main() { + group('ItemDetailsViewModel Tests -', () { + setUp(() => registerServices()); + tearDown(() => locator.reset()); + }); +}