diff --git a/assets/images/icon_profile.png b/assets/images/icon_profile.png new file mode 100644 index 0000000..f48afff Binary files /dev/null and b/assets/images/icon_profile.png differ diff --git a/assets/svg/icon_awards.svg b/assets/svg/icon_awards.svg new file mode 100644 index 0000000..67901d2 --- /dev/null +++ b/assets/svg/icon_awards.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/svg/icon_home.svg b/assets/svg/icon_home.svg new file mode 100644 index 0000000..a88c424 --- /dev/null +++ b/assets/svg/icon_home.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/svg/icon_shop.svg b/assets/svg/icon_shop.svg new file mode 100644 index 0000000..57c2ab6 --- /dev/null +++ b/assets/svg/icon_shop.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/common_ui/resources/my_assets.dart b/lib/common_ui/resources/my_assets.dart index 09d8c1c..dc5ec20 100644 --- a/lib/common_ui/resources/my_assets.dart +++ b/lib/common_ui/resources/my_assets.dart @@ -7,9 +7,13 @@ class MyAssets { static const String sample = 'assets/images/sample.png'; static const String shiaMind = 'assets/images/shia_mind.png'; static const String question = 'assets/images/question.png'; + static const String iconProfile = 'assets/images/icon_profile.png'; /// ----- Svg ----- static const String sampleSvg = 'assets/svg/sample.svg'; + static const String iconHome = 'assets/svg/icon_home.svg'; + static const String iconShop = 'assets/svg/icon_shop.svg'; + static const String iconAwards = 'assets/svg/icon_awards.svg'; /// ----- Audios ----- static const String sampleAudio = 'assets/audios/sample.mp3'; diff --git a/lib/core/params/home_params.dart b/lib/core/params/home_params.dart new file mode 100644 index 0000000..22518f3 --- /dev/null +++ b/lib/core/params/home_params.dart @@ -0,0 +1,13 @@ +class HomeParams { + int? id; + + HomeParams({this.id}); + + HomeParams copyWith({ + int? id, + }) { + return HomeParams( + id: id ?? this.id, + ); + } +} diff --git a/lib/core/routers/my_routes.dart b/lib/core/routers/my_routes.dart index 4fece1a..449cc34 100644 --- a/lib/core/routers/my_routes.dart +++ b/lib/core/routers/my_routes.dart @@ -1,3 +1,5 @@ +import 'package:shia_game_flutter/features/home/presentation/binding/home_binding.dart'; +import 'package:shia_game_flutter/features/home/presentation/pages/home_page.dart'; import 'package:shia_game_flutter/features/intro/presentation/binding/intro_binding.dart'; import 'package:shia_game_flutter/features/intro/presentation/ui/intro_page.dart'; import 'package:shia_game_flutter/features/sample/presentation/binding/sample_binding.dart'; @@ -11,6 +13,7 @@ class Routes { static const String samplePage = '/sample_page'; static const String introPage = '/intro_page'; + static const String homePage = '/home_page'; } List get appPages => [ @@ -24,4 +27,9 @@ List get appPages => [ page: () => const IntroPage(), binding: IntroBinding(), ), + GetPage( + name: Routes.homePage, + page: () => const HomePage(), + binding: HomeBinding(), + ), ]; diff --git a/lib/core/widgets/bottom_nav_bar/bottom_nav_bar.dart b/lib/core/widgets/bottom_nav_bar/bottom_nav_bar.dart new file mode 100644 index 0000000..44f08f1 --- /dev/null +++ b/lib/core/widgets/bottom_nav_bar/bottom_nav_bar.dart @@ -0,0 +1,74 @@ +import 'package:flutter/material.dart'; +import 'package:get/get_state_manager/src/rx_flutter/rx_obx_widget.dart'; +import 'package:get/get_state_manager/src/simple/get_view.dart'; +import 'package:shia_game_flutter/common_ui/resources/my_text_style.dart'; +import 'package:shia_game_flutter/features/home/presentation/controller/home_controller.dart'; +import 'package:shia_game_flutter/core/widgets/bottom_nav_bar/styles/bottom_nav_bar_item.dart'; +import 'package:shia_game_flutter/core/widgets/bottom_nav_bar/styles/bottom_nav_bar_profile_item.dart'; + +class BottomNavBar extends GetView { + const BottomNavBar({super.key}); + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + gradient: RadialGradient( + radius: 2.5, + colors: [Color(0XFF4F09BF), Color(0XFF250459)], + ), + ), + child: Obx( + () => BottomNavigationBar( + onTap: (int index) => controller.onChangeBottomNavBar(index), + currentIndex: controller.selectedIndex.value, + backgroundColor: Colors.transparent, + elevation: 0, + type: BottomNavigationBarType.fixed, + unselectedFontSize: 8, + selectedFontSize: 8, + unselectedLabelStyle: Lexend.bold, + selectedLabelStyle: Lexend.bold, + showSelectedLabels: false, + showUnselectedLabels: false, + items: List.generate( + controller.bottomNavList.length, + (index) => index == 3 + ? _bottomNavBarProfileItem(index) + : _bottomNavBarItem(index), + ), + ), + ), + ); + } + + BottomNavigationBarItem _bottomNavBarItem(int index) { + return BottomNavigationBarItem( + icon: Opacity( + opacity: 0.7, + child: BottomNavBarItem( + bottomNavEntity: controller.bottomNavList[index], + ), + ), + activeIcon: BottomNavBarItem( + bottomNavEntity: controller.bottomNavList[index], + ), + label: '', + ); + } + + BottomNavigationBarItem _bottomNavBarProfileItem(int index) { + return BottomNavigationBarItem( + icon: Opacity( + opacity: 0.7, + child: BottomNavBarProfileItem( + bottomNavEntity: controller.bottomNavList[index], + ), + ), + activeIcon: BottomNavBarProfileItem( + bottomNavEntity: controller.bottomNavList[index], + ), + label: '', + ); + } +} \ No newline at end of file diff --git a/lib/core/widgets/bottom_nav_bar/styles/bottom_nav_bar_item.dart b/lib/core/widgets/bottom_nav_bar/styles/bottom_nav_bar_item.dart new file mode 100644 index 0000000..f1c1a30 --- /dev/null +++ b/lib/core/widgets/bottom_nav_bar/styles/bottom_nav_bar_item.dart @@ -0,0 +1,26 @@ +import 'package:flutter/material.dart'; +import 'package:shia_game_flutter/common_ui/resources/my_spaces.dart'; +import 'package:shia_game_flutter/common_ui/resources/my_text_style.dart'; +import 'package:shia_game_flutter/core/utils/gap.dart'; +import 'package:shia_game_flutter/core/widgets/image/my_image.dart'; +import 'package:shia_game_flutter/features/home/domain/entity/bottom_nav_entity.dart'; + +class BottomNavBarItem extends StatelessWidget { + const BottomNavBarItem({super.key, required this.bottomNavEntity}); + + final BottomNavEntity bottomNavEntity; + + @override + Widget build(BuildContext context) { + return Column( + children: [ + MyImage(asset: bottomNavEntity.icon ?? ''), + MySpaces.s4.gapHeight, + Text( + bottomNavEntity.title ?? '', + style: Lexend.bold.copyWith(fontSize: MySpaces.s8), + ), + ], + ); + } +} diff --git a/lib/core/widgets/bottom_nav_bar/styles/bottom_nav_bar_profile_item.dart b/lib/core/widgets/bottom_nav_bar/styles/bottom_nav_bar_profile_item.dart new file mode 100644 index 0000000..23b0afe --- /dev/null +++ b/lib/core/widgets/bottom_nav_bar/styles/bottom_nav_bar_profile_item.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; +import 'package:shia_game_flutter/common_ui/resources/my_spaces.dart'; +import 'package:shia_game_flutter/common_ui/resources/my_text_style.dart'; +import 'package:shia_game_flutter/core/utils/gap.dart'; +import 'package:shia_game_flutter/core/widgets/image/my_image.dart'; +import 'package:shia_game_flutter/features/home/domain/entity/bottom_nav_entity.dart'; + +class BottomNavBarProfileItem extends StatelessWidget { + const BottomNavBarProfileItem({super.key, required this.bottomNavEntity}); + + final BottomNavEntity bottomNavEntity; + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Container( + height: 26, + width: 26, + padding: EdgeInsets.all(3), + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all( + width: 1, + color: Color(0XFFF4EEFF), + ) + ), + child: MyImage(asset: bottomNavEntity.icon ?? ''), + ), + MySpaces.s4.gapHeight, + Text( + bottomNavEntity.title ?? '', + style: Lexend.bold.copyWith(fontSize: MySpaces.s8), + ), + ], + ); + } +} diff --git a/lib/features/home/data/datasource/home_datasource.dart b/lib/features/home/data/datasource/home_datasource.dart new file mode 100644 index 0000000..5cb70c4 --- /dev/null +++ b/lib/features/home/data/datasource/home_datasource.dart @@ -0,0 +1,28 @@ +import 'package:shia_game_flutter/core/constants/my_api.dart'; +import 'package:shia_game_flutter/core/network/http_request.dart'; +import 'package:shia_game_flutter/core/params/home_params.dart'; +import 'package:shia_game_flutter/core/response/base_response.dart'; +import 'package:shia_game_flutter/features/home/data/model/home_model.dart'; +import 'package:shia_game_flutter/features/home/domain/entity/home_entity.dart'; + +abstract class IHomeDatasource { + Future getData({required HomeParams params}); +} + +class HomeDatasourceImpl implements IHomeDatasource { + final IHttpRequest httpRequest; + + const HomeDatasourceImpl(this.httpRequest); + + @override + Future getData({required HomeParams params}) async { + final response = await httpRequest.get( + path: MyApi.baseUrl, + ); + + return BaseResponse.getData( + response?['data'], + (json) => HomeModel.fromJson(json), + ); + } +} diff --git a/lib/features/home/data/model/home_model.dart b/lib/features/home/data/model/home_model.dart new file mode 100644 index 0000000..b1d2b84 --- /dev/null +++ b/lib/features/home/data/model/home_model.dart @@ -0,0 +1,13 @@ +import 'package:shia_game_flutter/features/home/domain/entity/home_entity.dart'; + +class HomeModel extends HomeEntity { + const HomeModel({ + super.id, + }); + + factory HomeModel.fromJson(Map json) { + return HomeModel( + id: json['id'], + ); + } +} diff --git a/lib/features/home/data/repository_impl/home_repository_impl.dart b/lib/features/home/data/repository_impl/home_repository_impl.dart new file mode 100644 index 0000000..203d244 --- /dev/null +++ b/lib/features/home/data/repository_impl/home_repository_impl.dart @@ -0,0 +1,29 @@ +import 'package:flutter/foundation.dart'; +import 'package:shia_game_flutter/core/error_handler/my_exception.dart'; +import 'package:shia_game_flutter/core/params/home_params.dart'; +import 'package:shia_game_flutter/core/utils/data_state.dart'; +import 'package:shia_game_flutter/features/home/data/datasource/home_datasource.dart'; +import 'package:shia_game_flutter/features/home/domain/entity/home_entity.dart'; +import 'package:shia_game_flutter/features/home/domain/repository/home_repository.dart'; + +class HomeRepositoryImpl implements IHomeRepository { + final IHomeDatasource datasource; + + const HomeRepositoryImpl(this.datasource); + + @override + Future> getData({required HomeParams params}) async { + try { + final HomeEntity response = await datasource.getData(params: params); + return DataState.success(response); + } on MyException catch (e) { + return DataState.error(e); + } catch (e) { + if (kDebugMode) { + rethrow; + } else { + return DataState.error(MyException(errorMessage: '$e')); + } + } + } +} diff --git a/lib/features/home/domain/entity/bottom_nav_entity.dart b/lib/features/home/domain/entity/bottom_nav_entity.dart new file mode 100644 index 0000000..43aea96 --- /dev/null +++ b/lib/features/home/domain/entity/bottom_nav_entity.dart @@ -0,0 +1,11 @@ +import 'package:equatable/equatable.dart'; + +class BottomNavEntity extends Equatable { + final String? icon; + final String? title; + + const BottomNavEntity({this.icon, this.title}); + + @override + List get props => [icon, title]; +} diff --git a/lib/features/home/domain/entity/home_entity.dart b/lib/features/home/domain/entity/home_entity.dart new file mode 100644 index 0000000..582723c --- /dev/null +++ b/lib/features/home/domain/entity/home_entity.dart @@ -0,0 +1,14 @@ +import 'package:equatable/equatable.dart'; + +class HomeEntity extends Equatable { + final int? id; + + const HomeEntity({ + this.id, + }); + + @override + List get props => [ + id, + ]; +} diff --git a/lib/features/home/domain/repository/home_repository.dart b/lib/features/home/domain/repository/home_repository.dart new file mode 100644 index 0000000..71709c3 --- /dev/null +++ b/lib/features/home/domain/repository/home_repository.dart @@ -0,0 +1,8 @@ +import 'package:shia_game_flutter/core/error_handler/my_exception.dart'; +import 'package:shia_game_flutter/core/params/home_params.dart'; +import 'package:shia_game_flutter/core/utils/data_state.dart'; +import 'package:shia_game_flutter/features/home/domain/entity/home_entity.dart'; + +abstract class IHomeRepository { + Future> getData({required HomeParams params}); +} diff --git a/lib/features/home/domain/usecases/get_home_usecase.dart b/lib/features/home/domain/usecases/get_home_usecase.dart new file mode 100644 index 0000000..dc2ad83 --- /dev/null +++ b/lib/features/home/domain/usecases/get_home_usecase.dart @@ -0,0 +1,17 @@ +import 'package:shia_game_flutter/core/error_handler/my_exception.dart'; +import 'package:shia_game_flutter/core/params/home_params.dart'; +import 'package:shia_game_flutter/core/usecase/usecase.dart'; +import 'package:shia_game_flutter/core/utils/data_state.dart'; +import 'package:shia_game_flutter/features/home/domain/entity/home_entity.dart'; +import 'package:shia_game_flutter/features/home/domain/repository/home_repository.dart'; + +class GetHomeUseCase implements UseCase { + final IHomeRepository repository; + + const GetHomeUseCase(this.repository); + + @override + Future> call(HomeParams params) { + return repository.getData(params: params); + } +} diff --git a/lib/features/home/presentation/binding/home_binding.dart b/lib/features/home/presentation/binding/home_binding.dart new file mode 100644 index 0000000..3c8ed17 --- /dev/null +++ b/lib/features/home/presentation/binding/home_binding.dart @@ -0,0 +1,9 @@ +import 'package:shia_game_flutter/features/home/presentation/controller/home_controller.dart'; +import 'package:get/get.dart'; + +class HomeBinding extends Bindings { + @override + void dependencies() { + Get.put(HomeController(Get.find())); + } +} diff --git a/lib/features/home/presentation/controller/home_controller.dart b/lib/features/home/presentation/controller/home_controller.dart new file mode 100644 index 0000000..aae9385 --- /dev/null +++ b/lib/features/home/presentation/controller/home_controller.dart @@ -0,0 +1,81 @@ +import 'package:flutter/cupertino.dart'; +import 'package:get/get.dart'; +import 'package:shia_game_flutter/common_ui/resources/my_assets.dart'; +import 'package:shia_game_flutter/core/params/home_params.dart'; +import 'package:shia_game_flutter/core/status/base_status.dart'; +import 'package:shia_game_flutter/core/utils/my_localization.dart'; +import 'package:shia_game_flutter/features/home/domain/entity/bottom_nav_entity.dart'; +import 'package:shia_game_flutter/features/home/domain/entity/home_entity.dart'; +import 'package:shia_game_flutter/features/home/domain/usecases/get_home_usecase.dart'; + +class HomeController extends GetxController with StateMixin { + /// ----- Constructor ----- + HomeController(this.getHomeUseCase); + + @override + void onInit() { + super.onInit(); + change('', status: RxStatus.success()); + } + + @override + void onClose() { + textEditingController.dispose(); + super.onClose(); + } + + /// ----- UseCases ----- + final GetHomeUseCase getHomeUseCase; + + /// ----- Variables ----- + final Rx homeParams = Rx(HomeParams()); + final Rx homeEntity = Rx(const HomeEntity()); + final List bottomNavList = [ + BottomNavEntity( + icon: MyAssets.iconHome, + title: Get.context?.translate.home, + ), + BottomNavEntity( + icon: MyAssets.iconShop, + title: Get.context?.translate.shop, + ), + BottomNavEntity( + icon: MyAssets.iconAwards, + title: Get.context?.translate.awards, + ), + BottomNavEntity( + icon: MyAssets.iconProfile, + title: Get.context?.translate.profile, + ), + ]; + + final Rx selectedIndex = Rx(0); + + + /// ------ Controllers ------ + final TextEditingController textEditingController = TextEditingController(); + + /// ------ Statuses ------ + final Rx getHomeStatus = Rx(const BaseInit()); + + /// ------ Functions ------ + void onChangeBottomNavBar(int index) { + selectedIndex.value = index; + } + + /// ------ Api Calls ------ + Future getHome() async { + change('', status: RxStatus.loading()); + await getHomeUseCase(homeParams.value).then( + (value) => value.fold( + (data) { + homeEntity.value = data; + change('', status: RxStatus.success()); + }, + (error) { + change('', status: RxStatus.error(error.errorMessage)); + }, + ), + ); + } +} diff --git a/lib/features/home/presentation/pages/home_page.dart b/lib/features/home/presentation/pages/home_page.dart new file mode 100644 index 0000000..e48ee8b --- /dev/null +++ b/lib/features/home/presentation/pages/home_page.dart @@ -0,0 +1,13 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:shia_game_flutter/features/home/presentation/controller/home_controller.dart'; +import 'package:shia_game_flutter/core/widgets/bottom_nav_bar/bottom_nav_bar.dart'; + +class HomePage extends GetView { + const HomePage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold(bottomNavigationBar: BottomNavBar()); + } +} diff --git a/lib/init_bindings.dart b/lib/init_bindings.dart index c1b1d7b..fd2d223 100644 --- a/lib/init_bindings.dart +++ b/lib/init_bindings.dart @@ -1,6 +1,10 @@ import 'package:get/get.dart'; import 'package:shia_game_flutter/core/network/http_request.dart'; import 'package:shia_game_flutter/core/network/http_request_impl.dart'; +import 'package:shia_game_flutter/features/home/data/datasource/home_datasource.dart'; +import 'package:shia_game_flutter/features/home/data/repository_impl/home_repository_impl.dart'; +import 'package:shia_game_flutter/features/home/domain/repository/home_repository.dart'; +import 'package:shia_game_flutter/features/home/domain/usecases/get_home_usecase.dart'; import 'package:shia_game_flutter/features/intro/data/datasource/intro_datasource.dart'; import 'package:shia_game_flutter/features/intro/data/repository_impl/intro_repository_impl.dart'; import 'package:shia_game_flutter/features/intro/domain/repository/intro_repository.dart'; @@ -23,4 +27,9 @@ void initBindings() { Get.lazyPut(() => IntroDatasourceImpl(Get.find())); Get.lazyPut(() => IntroRepositoryImpl(Get.find())); Get.lazyPut(() => GetIntroUseCase(Get.find())); + + /// ----- Home Feature ----- + Get.lazyPut(() => HomeDatasourceImpl(Get.find())); + Get.lazyPut(() => HomeRepositoryImpl(Get.find())); + Get.lazyPut(() => GetHomeUseCase(Get.find())); } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index ee262a0..cf03135 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -1,4 +1,8 @@ { "@@locale": "en", - "loading": "Loading..." + "loading": "Loading...", + "home": "Home", + "shop": "Shop", + "awards": "Awards", + "profile": "Profile" } \ No newline at end of file diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index f7b95b7..01e66b3 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -99,6 +99,30 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'Loading...'** String get loading; + + /// No description provided for @home. + /// + /// In en, this message translates to: + /// **'Home'** + String get home; + + /// No description provided for @shop. + /// + /// In en, this message translates to: + /// **'Shop'** + String get shop; + + /// No description provided for @awards. + /// + /// In en, this message translates to: + /// **'Awards'** + String get awards; + + /// No description provided for @profile. + /// + /// In en, this message translates to: + /// **'Profile'** + String get profile; } class _AppLocalizationsDelegate diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 3bb6aa4..bae517b 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -10,4 +10,16 @@ class AppLocalizationsEn extends AppLocalizations { @override String get loading => 'Loading...'; + + @override + String get home => 'Home'; + + @override + String get shop => 'Shop'; + + @override + String get awards => 'Awards'; + + @override + String get profile => 'Profile'; } diff --git a/lib/main.dart b/lib/main.dart index dedefa7..19bec5b 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -31,7 +31,7 @@ class MainApp extends StatelessWidget { fallbackLocale: const Locale('en', 'US'), supportedLocales: const [Locale('en', 'US')], getPages: appPages, - initialRoute: Routes.introPage, + initialRoute: Routes.homePage, localizationsDelegates: const [ AppLocalizations.delegate, GlobalMaterialLocalizations.delegate,