diff --git a/assets/images/bubble_chat_left.svg b/assets/images/bubble_chat_left.svg new file mode 100644 index 0000000..9d64c11 --- /dev/null +++ b/assets/images/bubble_chat_left.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/assets/images/bubble_chat_right.svg b/assets/images/bubble_chat_right.svg new file mode 100644 index 0000000..b8ab88d --- /dev/null +++ b/assets/images/bubble_chat_right.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/images/correct.svg b/assets/images/correct.svg new file mode 100644 index 0000000..17b0900 --- /dev/null +++ b/assets/images/correct.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/assets/images/diamond.png b/assets/images/diamond.png new file mode 100644 index 0000000..ba816b1 Binary files /dev/null and b/assets/images/diamond.png differ diff --git a/assets/images/done.svg b/assets/images/done.svg new file mode 100644 index 0000000..28d7f58 --- /dev/null +++ b/assets/images/done.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/images/hand_point.svg b/assets/images/hand_point.svg new file mode 100644 index 0000000..5c28ef9 --- /dev/null +++ b/assets/images/hand_point.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/images/home.svg b/assets/images/home.svg new file mode 100644 index 0000000..d9d0a2c --- /dev/null +++ b/assets/images/home.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/images/music.svg b/assets/images/music.svg new file mode 100644 index 0000000..03eb33b --- /dev/null +++ b/assets/images/music.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/images/pattern.png b/assets/images/pattern.png new file mode 100644 index 0000000..3cc6062 Binary files /dev/null and b/assets/images/pattern.png differ diff --git a/assets/images/persons.png b/assets/images/persons.png new file mode 100644 index 0000000..3438d1f Binary files /dev/null and b/assets/images/persons.png differ diff --git a/assets/images/wrong.svg b/assets/images/wrong.svg new file mode 100644 index 0000000..ebf05ff --- /dev/null +++ b/assets/images/wrong.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/lib/common_ui/resources/my_assets.dart b/lib/common_ui/resources/my_assets.dart index 8fc0a00..aafeaa3 100644 --- a/lib/common_ui/resources/my_assets.dart +++ b/lib/common_ui/resources/my_assets.dart @@ -17,4 +17,15 @@ class MyAssets { static const String language = 'assets/images/language.svg'; static const String newHorizon = 'assets/images/new_horizon.svg'; static const String khadijeLogo = 'assets/images/khadije_logo.png'; + static const String home = 'assets/images/home.svg'; + static const String music = 'assets/images/music.svg'; + static const String pattern = 'assets/images/pattern.png'; + static const String persons = 'assets/images/persons.png'; + static const String bubbleChatLeft = 'assets/images/bubble_chat_left.svg'; + static const String bubbleChatRight = 'assets/images/bubble_chat_right.svg'; + static const String diamond = 'assets/images/diamond.png'; + static const String done = 'assets/images/done.svg'; + static const String correct = 'assets/images/correct.svg'; + static const String wrong = 'assets/images/wrong.svg'; + static const String handPoint = 'assets/images/hand_point.svg'; } \ No newline at end of file diff --git a/lib/core/params/question_params.dart b/lib/core/params/question_params.dart new file mode 100644 index 0000000..0c6fff1 --- /dev/null +++ b/lib/core/params/question_params.dart @@ -0,0 +1,13 @@ +class QuestionParams { + int? id; + + QuestionParams({this.id}); + + QuestionParams copyWith({ + int? id, + }) { + return QuestionParams( + id: id ?? this.id, + ); + } +} diff --git a/lib/core/routers/my_routes.dart b/lib/core/routers/my_routes.dart index f6b6730..55f4ca7 100644 --- a/lib/core/routers/my_routes.dart +++ b/lib/core/routers/my_routes.dart @@ -3,6 +3,8 @@ import 'package:go_router/go_router.dart'; import 'package:hadi_hoda_flutter/core/utils/context_provider.dart'; import 'package:hadi_hoda_flutter/features/intro/presentation/bloc/intro_bloc.dart'; import 'package:hadi_hoda_flutter/features/intro/presentation/ui/intro_page.dart'; +import 'package:hadi_hoda_flutter/features/question/presentation/bloc/question_bloc.dart'; +import 'package:hadi_hoda_flutter/features/question/presentation/ui/question_page.dart'; import 'package:hadi_hoda_flutter/init_bindings.dart'; class Routes { @@ -11,6 +13,7 @@ class Routes { factory Routes() => _i; static const String introPage = '/intro_page'; + static const String questionPage = '/question_page'; } GoRouter get appPages => GoRouter( @@ -25,5 +28,13 @@ GoRouter get appPages => GoRouter( child: const IntroPage(), ), ), + GoRoute( + name: Routes.questionPage, + path: Routes.questionPage, + builder: (context, state) => BlocProvider( + create: (context) => QuestionBloc(locator()), + child: const QuestionPage(), + ), + ), ], ); diff --git a/lib/core/widgets/answer_box/answer_box.dart b/lib/core/widgets/answer_box/answer_box.dart index 05e4268..b22198e 100644 --- a/lib/core/widgets/answer_box/answer_box.dart +++ b/lib/core/widgets/answer_box/answer_box.dart @@ -22,7 +22,6 @@ class _AnswerBoxState extends State { }); }, child: SizedBox( - width: 170, child: Stack( alignment: Alignment.bottomCenter, clipBehavior: Clip.none, @@ -34,10 +33,7 @@ class _AnswerBoxState extends State { left: 0, right: 0, bottom: -36, - child: SizedBox( - height: 60, - child: AnswerTextBox(), - ), + child: AnswerTextBox(), ), ], ), diff --git a/lib/core/widgets/answer_box/styles/picture_box.dart b/lib/core/widgets/answer_box/styles/picture_box.dart index 7263d0f..1b9ab51 100644 --- a/lib/core/widgets/answer_box/styles/picture_box.dart +++ b/lib/core/widgets/answer_box/styles/picture_box.dart @@ -1,5 +1,8 @@ import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; import 'package:hadi_hoda_flutter/common_ui/resources/my_assets.dart'; +import 'package:hadi_hoda_flutter/common_ui/resources/my_spaces.dart'; +import 'package:hadi_hoda_flutter/common_ui/theme/my_theme.dart'; import 'package:hadi_hoda_flutter/core/utils/my_image.dart'; class AnswerPictureBox extends StatelessWidget { @@ -14,8 +17,53 @@ class AnswerPictureBox extends StatelessWidget { foregroundPainter: _SvgCustomPainter(selected), child: ClipPath( clipper: _SvgCustomClipper(), - child: MyImage( - image: MyAssets.backgroundIntro, fit: BoxFit.cover, size: 170), + child: Stack( + children: [ + MyImage( + image: MyAssets.backgroundIntro, + fit: BoxFit.cover, + size: 170, + ), + PositionedDirectional( + top: MySpaces.s12, + start: MySpaces.s12, + child: ClipPath( + clipper: _CountClipper(), + child: Container( + height: MySpaces.s32, + width: MySpaces.s32, + alignment: Alignment.center, + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Color(0XFF5732CB), + Color(0XFF322386), + ], + ), + ), + child: Text( + '1', + style: GoogleFonts.marhey( + fontSize: 17, + fontWeight: FontWeight.w600, + color: context.primaryColor, + ), + ), + ), + ), + ), + PositionedDirectional( + top: MySpaces.s14, + end: MySpaces.s12, + child: MyImage( + image: MyAssets.correct, + size: MySpaces.s40, + ), + ), + ], + ), ), ); } @@ -124,4 +172,38 @@ class _SvgCustomPainter extends CustomPainter { @override bool shouldRepaint(CustomPainter oldDelegate) => true; +} + +class _CountClipper extends CustomClipper { + @override + Path getClip(Size size) { + // Original SVG viewBox: width=34, height=33 + final sx = size.width / 34.0; + final sy = size.height / 33.0; + + final p = Path() + ..moveTo(33.3479 * sx, 14.8127 * sy) + ..cubicTo( + 33.3479 * sx, 23.7042 * sy, + 27.2015 * sx, 32.9501 * sy, + 17.8599 * sx, 32.9501 * sy, + )..cubicTo( + 8.51818 * sx, 32.9501 * sy, + 0.945251 * sx, 25.7421 * sy, + 0.945251 * sx, 16.8507 * sy, + )..cubicTo( + 0.945251 * sx, 7.95917 * sy, + 8.51818 * sx, 0.751205 * sy, + 17.8599 * sx, 0.751205 * sy, + )..cubicTo( + 27.2015 * sx, 0.751205 * sy, + 33.3479 * sx, 5.92127 * sy, + 33.3479 * sx, 14.8127 * sy, + )..close(); + + return p; + } + + @override + bool shouldReclip(covariant CustomClipper oldClipper) => false; } \ No newline at end of file diff --git a/lib/core/widgets/answer_box/styles/text_box.dart b/lib/core/widgets/answer_box/styles/text_box.dart index ed40b1d..c636cfa 100644 --- a/lib/core/widgets/answer_box/styles/text_box.dart +++ b/lib/core/widgets/answer_box/styles/text_box.dart @@ -1,39 +1,43 @@ import 'package:flutter/material.dart'; import 'package:google_fonts/google_fonts.dart'; +import 'package:hadi_hoda_flutter/common_ui/resources/my_spaces.dart'; class AnswerTextBox extends StatelessWidget { const AnswerTextBox({super.key}); @override Widget build(BuildContext context) { - return AspectRatio( - aspectRatio: 480 / 149.0, - child: Stack( - alignment: Alignment.center, - children: [ - Positioned.fill( - child: CustomPaint( - painter: _WavyBannerPainter(), - ), + return ClipPath( + clipper: _WavyBannerClipper(), + child: Container( + padding: EdgeInsets.all(MySpaces.s10), + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Color(0XFFFFFFFF), + Color(0XFFCADCFF), + ], ), - Text( - 'We walk in the yard with a glass of juice', - textAlign: TextAlign.center, - style: GoogleFonts.marhey( - fontSize: 12, - fontWeight: FontWeight.w600, - color: Color(0XFF322386) - ), - ) - ], + ), + child: Text( + 'We walk in the yard with a glass of juice ', + textAlign: TextAlign.center, + style: GoogleFonts.marhey( + fontSize: 14, + fontWeight: FontWeight.w700, + color: Color(0XFF322386) + ), + ), ), ); } } -class _WavyBannerPainter extends CustomPainter { +class _WavyBannerClipper extends CustomClipper { @override - void paint(Canvas canvas, Size size) { + Path getClip(Size size) { final sx = size.width / 480.0; final sy = size.height / 149.0; @@ -44,19 +48,9 @@ class _WavyBannerPainter extends CustomPainter { ..cubicTo(488.753 * sx, 118.634 * sy, 483.484 * sx, 26.7097 * sy, 459.365 * sx, 10.3813 * sy) ..cubicTo(435.246 * sx, -5.94701 * sy, 41.0302 * sx, -3.23423 * sy, 14.0623 * sx, 20.0845 * sy) ..close(); - - final paint = Paint() - ..style = PaintingStyle.fill - ..isAntiAlias = true - ..shader = const LinearGradient( - begin: Alignment.bottomCenter, - end: Alignment.topCenter, - colors: [Color(0xFFCADCFF), Colors.white], - ).createShader(Offset.zero & size); - - canvas.drawPath(path, paint); + return path; } @override - bool shouldRepaint(covariant _WavyBannerPainter oldDelegate) => false; -} + bool shouldReclip(covariant CustomClipper oldClipper) => false; +} \ No newline at end of file diff --git a/lib/core/widgets/showcase/question_showcase.dart b/lib/core/widgets/showcase/question_showcase.dart new file mode 100644 index 0000000..f6401c0 --- /dev/null +++ b/lib/core/widgets/showcase/question_showcase.dart @@ -0,0 +1,50 @@ +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'package:hadi_hoda_flutter/common_ui/resources/my_assets.dart'; +import 'package:hadi_hoda_flutter/common_ui/theme/my_theme.dart'; +import 'package:hadi_hoda_flutter/core/utils/my_image.dart'; +import 'package:hadi_hoda_flutter/core/utils/screen_size.dart'; +import 'package:showcaseview/showcaseview.dart'; + +class QuestionShowcase extends StatelessWidget { + const QuestionShowcase({ + super.key, + required this.globalKey, + required this.child, + this.description, + }); + + final GlobalKey globalKey; + final String? description; + final Widget child; + + @override + Widget build(BuildContext context) { + return Showcase( + key: globalKey, + blurValue: 10, + targetShapeBorder: CircleBorder(), + tooltipBackgroundColor: Colors.transparent, + disableMovingAnimation: true, + textColor: context.primaryColor, + descriptionTextAlign: TextAlign.center, + descTextStyle: GoogleFonts.marhey( + fontSize: 12, + fontWeight: FontWeight.w700, + color: context.primaryColor, + ), + disableScaleAnimation: true, + tooltipPadding: EdgeInsets.only(top: 60), + floatingActionWidget: FloatingActionWidget( + height: 60, + width: 60, + right: context.widthScreen * 0.17, + top: context.widthScreen * 1.17, + child: MyImage(image: MyAssets.handPoint), + ), + overlayColor: Color(0XFF0F0041), + description: description ?? '', + child: child, + ); + } +} diff --git a/lib/features/intro/presentation/ui/intro_page.dart b/lib/features/intro/presentation/ui/intro_page.dart index 4ef6fd6..bc2c0d5 100644 --- a/lib/features/intro/presentation/ui/intro_page.dart +++ b/lib/features/intro/presentation/ui/intro_page.dart @@ -1,10 +1,11 @@ import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; import 'package:hadi_hoda_flutter/common_ui/resources/my_assets.dart'; import 'package:hadi_hoda_flutter/common_ui/theme/my_theme.dart'; +import 'package:hadi_hoda_flutter/core/routers/my_routes.dart'; import 'package:hadi_hoda_flutter/core/utils/check_platform.dart'; import 'package:hadi_hoda_flutter/core/utils/my_image.dart'; import 'package:hadi_hoda_flutter/core/utils/screen_size.dart'; -import 'package:hadi_hoda_flutter/core/widgets/about_us_dialog/about_us_dialog.dart'; class IntroPage extends StatelessWidget { const IntroPage({super.key}); @@ -79,7 +80,7 @@ class IntroPage extends StatelessWidget { size: checkSize(context: context, mobile: 90, tablet: 160), ), onTap: () { - showAboutUsDialog(context: context); + context.goNamed(Routes.questionPage); }, ), MyImage( diff --git a/lib/features/question/data/datasource/question_datasource.dart b/lib/features/question/data/datasource/question_datasource.dart new file mode 100644 index 0000000..2142b93 --- /dev/null +++ b/lib/features/question/data/datasource/question_datasource.dart @@ -0,0 +1,28 @@ +import 'package:hadi_hoda_flutter/core/constants/my_api.dart'; +import 'package:hadi_hoda_flutter/core/network/http_request.dart'; +import 'package:hadi_hoda_flutter/core/params/question_params.dart'; +import 'package:hadi_hoda_flutter/core/response/base_response.dart'; +import 'package:hadi_hoda_flutter/features/question/data/model/question_model.dart'; +import 'package:hadi_hoda_flutter/features/question/domain/entity/question_entity.dart'; + +abstract class IQuestionDatasource { + Future getData({required QuestionParams params}); +} + +class QuestionDatasourceImpl implements IQuestionDatasource { + final IHttpRequest httpRequest; + + const QuestionDatasourceImpl(this.httpRequest); + + @override + Future getData({required QuestionParams params}) async { + final response = await httpRequest.get( + path: MyApi.baseUrl, + ); + + return BaseResponse.getData( + response?['data'], + (json) => QuestionModel.fromJson(json), + ); + } +} diff --git a/lib/features/question/data/model/question_model.dart b/lib/features/question/data/model/question_model.dart new file mode 100644 index 0000000..489b66c --- /dev/null +++ b/lib/features/question/data/model/question_model.dart @@ -0,0 +1,13 @@ +import 'package:hadi_hoda_flutter/features/question/domain/entity/question_entity.dart'; + +class QuestionModel extends QuestionEntity { + const QuestionModel({ + super.id, + }); + + factory QuestionModel.fromJson(Map json) { + return QuestionModel( + id: json['id'], + ); + } +} diff --git a/lib/features/question/data/repository_impl/question_repository_impl.dart b/lib/features/question/data/repository_impl/question_repository_impl.dart new file mode 100644 index 0000000..af5a1c6 --- /dev/null +++ b/lib/features/question/data/repository_impl/question_repository_impl.dart @@ -0,0 +1,29 @@ +import 'package:hadi_hoda_flutter/core/params/question_params.dart'; +import 'package:flutter/foundation.dart'; +import 'package:hadi_hoda_flutter/core/error_handler/my_exception.dart'; +import 'package:hadi_hoda_flutter/core/utils/data_state.dart'; +import 'package:hadi_hoda_flutter/features/question/data/datasource/question_datasource.dart'; +import 'package:hadi_hoda_flutter/features/question/domain/entity/question_entity.dart'; +import 'package:hadi_hoda_flutter/features/question/domain/repository/question_repository.dart'; + +class QuestionRepositoryImpl implements IQuestionRepository { + final IQuestionDatasource datasource; + + const QuestionRepositoryImpl(this.datasource); + + @override + Future> getData({required QuestionParams params}) async { + try { + final QuestionEntity 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/question/domain/entity/question_entity.dart b/lib/features/question/domain/entity/question_entity.dart new file mode 100644 index 0000000..377f2b1 --- /dev/null +++ b/lib/features/question/domain/entity/question_entity.dart @@ -0,0 +1,14 @@ +import 'package:equatable/equatable.dart'; + +class QuestionEntity extends Equatable { + final int? id; + + const QuestionEntity({ + this.id, + }); + + @override + List get props => [ + id, + ]; +} diff --git a/lib/features/question/domain/repository/question_repository.dart b/lib/features/question/domain/repository/question_repository.dart new file mode 100644 index 0000000..3c9e008 --- /dev/null +++ b/lib/features/question/domain/repository/question_repository.dart @@ -0,0 +1,8 @@ +import 'package:hadi_hoda_flutter/core/error_handler/my_exception.dart'; +import 'package:hadi_hoda_flutter/core/params/question_params.dart'; +import 'package:hadi_hoda_flutter/core/utils/data_state.dart'; +import 'package:hadi_hoda_flutter/features/question/domain/entity/question_entity.dart'; + +abstract class IQuestionRepository { + Future> getData({required QuestionParams params}); +} diff --git a/lib/features/question/domain/usecases/get_question_usecase.dart b/lib/features/question/domain/usecases/get_question_usecase.dart new file mode 100644 index 0000000..697534b --- /dev/null +++ b/lib/features/question/domain/usecases/get_question_usecase.dart @@ -0,0 +1,17 @@ +import 'package:hadi_hoda_flutter/core/error_handler/my_exception.dart'; +import 'package:hadi_hoda_flutter/core/params/question_params.dart'; +import 'package:hadi_hoda_flutter/core/usecase/usecase.dart'; +import 'package:hadi_hoda_flutter/core/utils/data_state.dart'; +import 'package:hadi_hoda_flutter/features/question/domain/entity/question_entity.dart'; +import 'package:hadi_hoda_flutter/features/question/domain/repository/question_repository.dart'; + +class GetQuestionUseCase implements UseCase { + final IQuestionRepository repository; + + const GetQuestionUseCase(this.repository); + + @override + Future> call(QuestionParams params) { + return repository.getData(params: params); + } +} diff --git a/lib/features/question/presentation/bloc/question_bloc.dart b/lib/features/question/presentation/bloc/question_bloc.dart new file mode 100644 index 0000000..92706ac --- /dev/null +++ b/lib/features/question/presentation/bloc/question_bloc.dart @@ -0,0 +1,52 @@ +import 'dart:async'; +import 'package:bloc/bloc.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:hadi_hoda_flutter/core/status/base_status.dart'; +import 'package:hadi_hoda_flutter/features/question/domain/entity/question_entity.dart'; +import 'package:hadi_hoda_flutter/features/question/domain/usecases/get_question_usecase.dart'; +import 'package:hadi_hoda_flutter/features/question/presentation/bloc/question_event.dart'; +import 'package:hadi_hoda_flutter/features/question/presentation/bloc/question_state.dart'; +import 'package:showcaseview/showcaseview.dart'; + +class QuestionBloc extends Bloc { + /// ------------constructor------------ + QuestionBloc( + this._getQuestionUseCase, + ) : super(const QuestionState()) { + on(_getQuestionEvent); + } + + /// ------------UseCases------------ + final GetQuestionUseCase _getQuestionUseCase; + + /// ------------Variables------------ + final List keys = [ + GlobalKey(), + GlobalKey(), + GlobalKey(), + GlobalKey(), + ]; + + /// ------------Controllers------------ + + /// ------------Functions------------ + void startShowCase({required BuildContext context}) { + ShowCaseWidget.of(context).startShowCase([keys[1]]); + } + + /// ------------Api Calls------------ + FutureOr _getQuestionEvent(event, emit) async { + await _getQuestionUseCase(event.questionParams).then( + (value) { + value.fold( + (data) { + emit(state.copyWith(getQuestionStatus: BaseComplete(data))); + }, + (error) { + emit(state.copyWith(getQuestionStatus: BaseError(error.errorMessage))); + }, + ); + }, + ); + } +} diff --git a/lib/features/question/presentation/bloc/question_event.dart b/lib/features/question/presentation/bloc/question_event.dart new file mode 100644 index 0000000..b4c4b21 --- /dev/null +++ b/lib/features/question/presentation/bloc/question_event.dart @@ -0,0 +1,5 @@ +sealed class QuestionEvent { + const QuestionEvent(); +} + +class GetQuestionEvent extends QuestionEvent {} diff --git a/lib/features/question/presentation/bloc/question_state.dart b/lib/features/question/presentation/bloc/question_state.dart new file mode 100644 index 0000000..3c1b18f --- /dev/null +++ b/lib/features/question/presentation/bloc/question_state.dart @@ -0,0 +1,15 @@ +import 'package:hadi_hoda_flutter/core/status/base_status.dart'; + +class QuestionState { + final BaseStatus getQuestionStatus; + + const QuestionState({this.getQuestionStatus = const BaseInit()}); + + QuestionState copyWith({ + BaseStatus? getQuestionStatus, + }) { + return QuestionState( + getQuestionStatus: getQuestionStatus ?? this.getQuestionStatus, + ); + } +} diff --git a/lib/features/question/presentation/ui/question_page.dart b/lib/features/question/presentation/ui/question_page.dart new file mode 100644 index 0000000..abb2798 --- /dev/null +++ b/lib/features/question/presentation/ui/question_page.dart @@ -0,0 +1,178 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'package:hadi_hoda_flutter/common_ui/resources/my_assets.dart'; +import 'package:hadi_hoda_flutter/common_ui/resources/my_spaces.dart'; +import 'package:hadi_hoda_flutter/core/utils/gap.dart'; +import 'package:hadi_hoda_flutter/core/utils/my_image.dart'; +import 'package:hadi_hoda_flutter/core/utils/my_localization.dart'; +import 'package:hadi_hoda_flutter/core/utils/screen_size.dart'; +import 'package:hadi_hoda_flutter/core/widgets/answer_box/answer_box.dart'; +import 'package:hadi_hoda_flutter/core/widgets/showcase/question_showcase.dart'; +import 'package:hadi_hoda_flutter/features/question/presentation/bloc/question_bloc.dart'; +import 'package:hadi_hoda_flutter/features/question/presentation/ui/widgets/glassy_button.dart'; +import 'package:hadi_hoda_flutter/features/question/presentation/ui/widgets/left_blob.dart'; +import 'package:hadi_hoda_flutter/features/question/presentation/ui/widgets/question_stepper.dart'; +import 'package:hadi_hoda_flutter/features/question/presentation/ui/widgets/refresh_button.dart'; +import 'package:hadi_hoda_flutter/features/question/presentation/ui/widgets/right_blob.dart'; +import 'package:showcaseview/showcaseview.dart'; + +class QuestionPage extends StatelessWidget { + const QuestionPage({super.key}); + + @override + Widget build(BuildContext context) { + return ShowCaseWidget( + builder: (context) { + return Scaffold( + body: Container( + height: context.heightScreen, + width: context.widthScreen, + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [Color(0XFF6930DA), Color(0XFF263AA1)], + ), + image: DecorationImage( + image: AssetImage(MyAssets.pattern), + scale: 3, + repeat: ImageRepeat.repeat, + colorFilter: ColorFilter.mode( + Colors.black.withValues(alpha: 0.3), + BlendMode.srcIn, + ), + ), + ), + child: SafeArea( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: MySpaces.s16), + child: Column( + children: [ + MySpaces.s4.gapHeight, + _topButtons(), + MySpaces.s10.gapHeight, + QuestionStepper(), + _titles(), + MySpaces.s14.gapHeight, + _questions(), + _bottomDetail(context), + ], + ), + ), + ), + ), + ); + }, + ); + } + + Widget _topButtons() { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + GlassyButton(image: MyAssets.home, onTap: () {}), + Text( + 'Toothbrushing etiquette', + style: GoogleFonts.marhey( + fontSize: 14, + fontWeight: FontWeight.w700, + color: Colors.white, + ), + ), + GlassyButton(image: MyAssets.music, onTap: () {}), + ], + ); + } + + Column _titles() { + return Column( + spacing: MySpaces.s4, + children: [ + Text( + 'Question 1 / 5', + style: GoogleFonts.marhey( + fontSize: 12, + fontWeight: FontWeight.w500, + color: Colors.white.withValues(alpha: 0.5), + shadows: [ + Shadow( + offset: Offset(0, 1), + blurRadius: 1, + color: Color(0xFF000000).withValues(alpha: 0.25), + ), + ], + ), + ), + Text( + 'Heda wants her teeth to be clean. Which of her actions do you think is correct?', + textAlign: TextAlign.center, + style: GoogleFonts.marhey( + fontSize: 22, + fontWeight: FontWeight.w600, + color: Colors.white, + shadows: [ + Shadow( + offset: Offset(0, 1), + blurRadius: 1, + color: Color(0xFF000000).withValues(alpha: 0.25), + ), + ], + ), + ), + ], + ); + } + + Expanded _questions() { + return Expanded( + child: GridView.builder( + itemCount: 4, + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + crossAxisSpacing: MySpaces.s20, + mainAxisSpacing: 50, + ), + itemBuilder: (context, index) => QuestionShowcase( + globalKey: context.read().keys[index], + description: context.translate.tap_to_select, + child: AnswerBox(), + ), + ), + ); + } + + Widget _bottomDetail(BuildContext context) { + return SizedBox( + width: context.widthScreen, + child: Stack( + clipBehavior: Clip.none, + alignment: Alignment.center, + children: [ + PositionedDirectional( + start: 0, + top: -10, + child: LeftBlob(), + ), + Padding( + padding: const EdgeInsetsDirectional.only(end: 60), + child: MyImage(image: MyAssets.persons), + ), + PositionedDirectional( + start: 210, + top: -20, + child: RightBlob(), + ), + PositionedDirectional( + end: 0, + bottom: 10, + child: RefreshButton( + onTap: () {}, + ), + ), + ], + ), + ); + } +} diff --git a/lib/features/question/presentation/ui/widgets/glassy_button.dart b/lib/features/question/presentation/ui/widgets/glassy_button.dart new file mode 100644 index 0000000..e346f4d --- /dev/null +++ b/lib/features/question/presentation/ui/widgets/glassy_button.dart @@ -0,0 +1,41 @@ +import 'package:flutter/material.dart'; +import 'package:hadi_hoda_flutter/common_ui/resources/my_spaces.dart'; +import 'package:hadi_hoda_flutter/core/utils/my_image.dart'; + +class GlassyButton extends StatelessWidget { + const GlassyButton({super.key, required this.image, this.onTap}); + + final String image; + final VoidCallback? onTap; + + @override + Widget build(BuildContext context) { + return Material( + color: Colors.transparent, + child: Ink( + width: 48, + height: 48, + decoration: BoxDecoration( + shape: BoxShape.circle, + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Colors.white.withValues(alpha: 0.3), + Color(0XFF6930DA).withValues(alpha: 0.1), + ], + ), + border: Border.all(color: Colors.white.withValues(alpha: 0.3)), + ), + child: InkWell( + onTap: onTap, + borderRadius: BorderRadius.all(Radius.circular(100)), + child: Padding( + padding: const EdgeInsets.all(MySpaces.s12), + child: MyImage(image: image), + ), + ), + ), + ); + } +} diff --git a/lib/features/question/presentation/ui/widgets/left_blob.dart b/lib/features/question/presentation/ui/widgets/left_blob.dart new file mode 100644 index 0000000..0964766 --- /dev/null +++ b/lib/features/question/presentation/ui/widgets/left_blob.dart @@ -0,0 +1,27 @@ +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'package:hadi_hoda_flutter/common_ui/resources/my_assets.dart'; +import 'package:hadi_hoda_flutter/core/utils/my_image.dart'; + +class LeftBlob extends StatelessWidget { + const LeftBlob({super.key}); + + @override + Widget build(BuildContext context) { + return Stack( + alignment: Alignment.center, + children: [ + MyImage(image: MyAssets.bubbleChatLeft), + Text( + 'Your answer\nwas not correct.', + textAlign: TextAlign.center, + style: GoogleFonts.marhey( + fontSize: 12, + fontWeight: FontWeight.w500, + color: Color(0XFFB5AEEE), + ), + ), + ], + ); + } +} diff --git a/lib/features/question/presentation/ui/widgets/question_stepper.dart b/lib/features/question/presentation/ui/widgets/question_stepper.dart new file mode 100644 index 0000000..c0fbf9d --- /dev/null +++ b/lib/features/question/presentation/ui/widgets/question_stepper.dart @@ -0,0 +1,102 @@ +import 'package:easy_stepper/easy_stepper.dart'; +import 'package:flutter/material.dart'; +import 'package:hadi_hoda_flutter/common_ui/resources/my_assets.dart'; +import 'package:hadi_hoda_flutter/core/utils/my_image.dart'; + +class QuestionStepper extends StatelessWidget { + const QuestionStepper({super.key}); + + @override + Widget build(BuildContext context) { + return SizedBox( + height: 80, + child: EasyStepper( + activeStep: 1, + lineStyle: LineStyle( + lineLength: 20, + lineType: LineType.normal, + defaultLineColor: Color(0XFFDFDDF6), + lineThickness: 5, + finishedLineColor: Color(0XFF21B738), + ), + activeStepBackgroundColor: Colors.transparent, + finishedStepBackgroundColor: Colors.transparent, + unreachedStepBackgroundColor: Colors.transparent, + internalPadding: 0, + showLoadingAnimation: false, + stepRadius: 18, + showStepBorder: false, + padding: EdgeInsets.all(0), + enableStepTapping: false, + steps: List.generate( + 6, + (index) => EasyStep( + customStep: index == 5 + ? MyImage(image: MyAssets.diamond, size: 50) + : ClipPath( + clipper: _StepperClipper(), + child: Container( + height: 32, + width: 32, + padding: EdgeInsets.all(4), + decoration: BoxDecoration( + color: Color(0XFFDFDDF6), + shape: BoxShape.circle, + ), + child: ClipPath( + clipper: _StepperClipper(), + child: Container( + padding: EdgeInsets.all(6), + decoration: BoxDecoration( + shape: BoxShape.circle, + color: index < 1 + ? Color(0XFF21B738) + : index == 1 + ? Color(0XFF847AC4) + : Colors.transparent, + ), + child: index < 1 ? MyImage(image: MyAssets.done) : null, + ), + ), + ), + ), + ), + ), + ), + ); + } +} + +class _StepperClipper extends CustomClipper { + @override + Path getClip(Size size) { + // Original SVG viewBox: width=34, height=33 + final sx = size.width / 34.0; + final sy = size.height / 33.0; + + final p = Path() + ..moveTo(33.3479 * sx, 14.8127 * sy) + ..cubicTo( + 33.3479 * sx, 23.7042 * sy, + 27.2015 * sx, 32.9501 * sy, + 17.8599 * sx, 32.9501 * sy, + )..cubicTo( + 8.51818 * sx, 32.9501 * sy, + 0.945251 * sx, 25.7421 * sy, + 0.945251 * sx, 16.8507 * sy, + )..cubicTo( + 0.945251 * sx, 7.95917 * sy, + 8.51818 * sx, 0.751205 * sy, + 17.8599 * sx, 0.751205 * sy, + )..cubicTo( + 27.2015 * sx, 0.751205 * sy, + 33.3479 * sx, 5.92127 * sy, + 33.3479 * sx, 14.8127 * sy, + )..close(); + + return p; + } + + @override + bool shouldReclip(covariant CustomClipper oldClipper) => false; +} diff --git a/lib/features/question/presentation/ui/widgets/refresh_button.dart b/lib/features/question/presentation/ui/widgets/refresh_button.dart new file mode 100644 index 0000000..801678e --- /dev/null +++ b/lib/features/question/presentation/ui/widgets/refresh_button.dart @@ -0,0 +1,32 @@ +import 'package:flutter/material.dart'; +import 'package:hadi_hoda_flutter/common_ui/theme/my_theme.dart'; + +class RefreshButton extends StatelessWidget { + const RefreshButton({super.key, this.onTap,}); + + final VoidCallback? onTap; + + @override + Widget build(BuildContext context) { + return Material( + color: context.noColor, + child: Ink( + height: 48, + width: 48, + decoration: BoxDecoration( + shape: BoxShape.circle, + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [Color(0XFFA393FF), Color(0XFFC6BCFB)], + ), + ), + child: InkWell( + onTap: onTap, + borderRadius: BorderRadius.all(Radius.circular(100)), + child: Icon(Icons.refresh, size: 40, color: Color(0XFF263AA1)), + ), + ), + ); + } +} diff --git a/lib/features/question/presentation/ui/widgets/right_blob.dart b/lib/features/question/presentation/ui/widgets/right_blob.dart new file mode 100644 index 0000000..b10237a --- /dev/null +++ b/lib/features/question/presentation/ui/widgets/right_blob.dart @@ -0,0 +1,27 @@ +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'package:hadi_hoda_flutter/common_ui/resources/my_assets.dart'; +import 'package:hadi_hoda_flutter/core/utils/my_image.dart'; + +class RightBlob extends StatelessWidget { + const RightBlob({super.key}); + + @override + Widget build(BuildContext context) { + return Stack( + alignment: Alignment.center, + children: [ + MyImage(image: MyAssets.bubbleChatRight), + Text( + 'Be more\ncareful.', + textAlign: TextAlign.center, + style: GoogleFonts.marhey( + fontSize: 12, + fontWeight: FontWeight.w500, + color: Color(0XFFB5AEEE), + ), + ), + ], + ); + } +} diff --git a/lib/init_bindings.dart b/lib/init_bindings.dart index faea518..8bb826b 100644 --- a/lib/init_bindings.dart +++ b/lib/init_bindings.dart @@ -4,6 +4,10 @@ import 'package:hadi_hoda_flutter/features/intro/data/datasource/intro_datasourc import 'package:hadi_hoda_flutter/features/intro/data/repository_impl/intro_repository_impl.dart'; import 'package:hadi_hoda_flutter/features/intro/domain/repository/intro_repository.dart'; import 'package:hadi_hoda_flutter/features/intro/domain/usecases/get_intro_usecase.dart'; +import 'package:hadi_hoda_flutter/features/question/data/datasource/question_datasource.dart'; +import 'package:hadi_hoda_flutter/features/question/data/repository_impl/question_repository_impl.dart'; +import 'package:hadi_hoda_flutter/features/question/domain/repository/question_repository.dart'; +import 'package:hadi_hoda_flutter/features/question/domain/usecases/get_question_usecase.dart'; import 'package:hadi_hoda_flutter/features/sample/data/datasource/sample_datasource.dart'; import 'package:hadi_hoda_flutter/features/sample/data/repository_impl/sample_repository_impl.dart'; import 'package:hadi_hoda_flutter/features/sample/domain/repository/sample_repository.dart'; @@ -25,4 +29,9 @@ void initBindings() { locator.registerLazySingleton(() => IntroDatasourceImpl(locator())); locator.registerLazySingleton(() => IntroRepositoryImpl(locator())); locator.registerLazySingleton(() => GetIntroUseCase(locator())); + + /// Question Feature + locator.registerLazySingleton(() => QuestionDatasourceImpl(locator())); + locator.registerLazySingleton(() => QuestionRepositoryImpl(locator())); + locator.registerLazySingleton(() => GetQuestionUseCase(locator())); } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 41f04cd..f21e3f9 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -1,4 +1,5 @@ { "about_us": "About us", - "about_us_desc" : "Rive combines an interactive design tool, a new stateful graphics format, a lightweight multi-platform runtime, and a blazing-fast vector renderer. \nThis end-to-end pipeline brings interfaces to life with motion. It gives designers and devs the tools to build." + "about_us_desc" : "Rive combines an interactive design tool, a new stateful graphics format, a lightweight multi-platform runtime, and a blazing-fast vector renderer. \nThis end-to-end pipeline brings interfaces to life with motion. It gives designers and devs the tools to build.", + "tap_to_select": "Tap the correct option to select." } \ No newline at end of file diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index b1de57e..6e94635 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -105,6 +105,12 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'Rive combines an interactive design tool, a new stateful graphics format, a lightweight multi-platform runtime, and a blazing-fast vector renderer. \nThis end-to-end pipeline brings interfaces to life with motion. It gives designers and devs the tools to build.'** String get about_us_desc; + + /// No description provided for @tap_to_select. + /// + /// In en, this message translates to: + /// **'Tap the correct option to select.'** + String get tap_to_select; } class _AppLocalizationsDelegate diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 6967cb9..6b8bd59 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -14,4 +14,7 @@ class AppLocalizationsEn extends AppLocalizations { @override String get about_us_desc => 'Rive combines an interactive design tool, a new stateful graphics format, a lightweight multi-platform runtime, and a blazing-fast vector renderer. \nThis end-to-end pipeline brings interfaces to life with motion. It gives designers and devs the tools to build.'; + + @override + String get tap_to_select => 'Tap the correct option to select.'; } diff --git a/pubspec.lock b/pubspec.lock index 51806b4..b9bc647 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1,6 +1,14 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + archive: + dependency: transitive + description: + name: archive + sha256: "2fde1607386ab523f7a36bb3e7edb43bd58e6edaf2ffb29d8a6d578b297fdbbd" + url: "https://pub.dev" + source: hosted + version: "4.0.7" args: dependency: transitive description: @@ -81,6 +89,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.1" + easy_stepper: + dependency: "direct main" + description: + name: easy_stepper + sha256: "63f66314a509ec690c8152a41288961fd96ba9e92ef184299f068a5e78bd16ad" + url: "https://pub.dev" + source: hosted + version: "0.8.5+1" equatable: dependency: "direct main" description: @@ -245,6 +261,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.0" + lottie: + dependency: transitive + description: + name: lottie + sha256: "8ae0be46dbd9e19641791dc12ee480d34e1fd3f84c749adc05f3ad9342b71b95" + url: "https://pub.dev" + source: hosted + version: "3.3.2" matcher: dependency: transitive description: @@ -373,6 +397,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.8" + posix: + dependency: transitive + description: + name: posix + sha256: "6323a5b0fa688b6a010df4905a56b00181479e6d10534cecfecede2aa55add61" + url: "https://pub.dev" + source: hosted + version: "6.0.3" pretty_dio_logger: dependency: "direct main" description: @@ -445,6 +477,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.4.1" + showcaseview: + dependency: "direct main" + description: + name: showcaseview + sha256: "82e013ac2de1ae92cc6e652badf676606057c8e17aa3afd91e78866c4b4e85b1" + url: "https://pub.dev" + source: hosted + version: "4.0.1" sky_engine: dependency: transitive description: flutter @@ -572,4 +612,4 @@ packages: version: "6.6.1" sdks: dart: ">=3.9.2 <4.0.0" - flutter: ">=3.29.0" + flutter: ">=3.35.0" diff --git a/pubspec.yaml b/pubspec.yaml index dd2a1d6..7b50df4 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -9,6 +9,7 @@ environment: dependencies: bloc: ^9.0.0 dio: ^5.9.0 + easy_stepper: ^0.8.5+1 equatable: ^2.0.7 flutter: sdk: flutter @@ -22,6 +23,7 @@ dependencies: intl: ^0.20.2 pretty_dio_logger: ^1.4.0 shared_preferences: ^2.5.3 + showcaseview: ^4.0.1 dev_dependencies: flutter_test: