Compare commits

...
12 Commits
Author SHA1 Message Date
aefril 2560c1890b Merge pull request 'dev' (#2) from dev into main
Reviewed-on: #2
2026-01-18 16:03:39 +00:00
Efril 7887704a5c update service type 2026-01-16 16:07:56 +07:00
Efril 4c12244d3c checkout page 2026-01-16 15:53:06 +07:00
Efril f4f775b9ed menu detail page 2026-01-16 14:41:09 +07:00
Efril 44d48d41d4 menu page 2026-01-16 13:27:09 +07:00
efrilm 82e1f93cce update 2026-01-16 12:26:06 +07:00
efrilm 1ce980a87b mystery box 2025-10-12 13:27:39 +07:00
aefril a09fda0585 Merge pull request 'dev' (#1) from dev into main
Reviewed-on: #1
2025-09-18 08:07:46 +00:00
efrilm 909c312af0 Ferish Wheel and Music 2025-09-18 14:53:39 +07:00
efrilm 73918430b2 Customer Point 2025-09-18 13:01:31 +07:00
efrilm a5d66c63b7 State Is Authenticated 2025-09-18 12:25:48 +07:00
efrilm 3c596461c6 Game Prize and Logout 2025-09-18 10:39:54 +07:00
99 changed files with 9728 additions and 1519 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 332 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 367 KiB

@@ -0,0 +1,37 @@
import 'package:bloc/bloc.dart';
import 'package:dartz/dartz.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:injectable/injectable.dart';
import '../../../domain/auth/auth.dart';
part 'logout_form_event.dart';
part 'logout_form_state.dart';
part 'logout_form_bloc.freezed.dart';
@injectable
class LogoutFormBloc extends Bloc<LogoutFormEvent, LogoutFormState> {
final IAuthRepository _repository;
LogoutFormBloc(this._repository) : super(LogoutFormState.initial()) {
on<LogoutFormEvent>(_onLogoutFormEvent);
}
Future<void> _onLogoutFormEvent(
LogoutFormEvent event,
Emitter<LogoutFormState> emit,
) {
return event.map(
submitted: (e) async {
emit(state.copyWith(isSubmitting: true, failureOrAuthOption: none()));
final failureOrAuth = await _repository.logout();
emit(
state.copyWith(
isSubmitting: false,
failureOrAuthOption: optionOf(failureOrAuth),
),
);
},
);
}
}
@@ -0,0 +1,335 @@
// 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 'logout_form_bloc.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(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',
);
/// @nodoc
mixin _$LogoutFormEvent {
@optionalTypeArgs
TResult when<TResult extends Object?>({
required TResult Function() submitted,
}) => throw _privateConstructorUsedError;
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult? Function()? submitted,
}) => throw _privateConstructorUsedError;
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
TResult Function()? submitted,
required TResult orElse(),
}) => throw _privateConstructorUsedError;
@optionalTypeArgs
TResult map<TResult extends Object?>({
required TResult Function(_Submitted value) submitted,
}) => throw _privateConstructorUsedError;
@optionalTypeArgs
TResult? mapOrNull<TResult extends Object?>({
TResult? Function(_Submitted value)? submitted,
}) => throw _privateConstructorUsedError;
@optionalTypeArgs
TResult maybeMap<TResult extends Object?>({
TResult Function(_Submitted value)? submitted,
required TResult orElse(),
}) => throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $LogoutFormEventCopyWith<$Res> {
factory $LogoutFormEventCopyWith(
LogoutFormEvent value,
$Res Function(LogoutFormEvent) then,
) = _$LogoutFormEventCopyWithImpl<$Res, LogoutFormEvent>;
}
/// @nodoc
class _$LogoutFormEventCopyWithImpl<$Res, $Val extends LogoutFormEvent>
implements $LogoutFormEventCopyWith<$Res> {
_$LogoutFormEventCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of LogoutFormEvent
/// with the given fields replaced by the non-null parameter values.
}
/// @nodoc
abstract class _$$SubmittedImplCopyWith<$Res> {
factory _$$SubmittedImplCopyWith(
_$SubmittedImpl value,
$Res Function(_$SubmittedImpl) then,
) = __$$SubmittedImplCopyWithImpl<$Res>;
}
/// @nodoc
class __$$SubmittedImplCopyWithImpl<$Res>
extends _$LogoutFormEventCopyWithImpl<$Res, _$SubmittedImpl>
implements _$$SubmittedImplCopyWith<$Res> {
__$$SubmittedImplCopyWithImpl(
_$SubmittedImpl _value,
$Res Function(_$SubmittedImpl) _then,
) : super(_value, _then);
/// Create a copy of LogoutFormEvent
/// with the given fields replaced by the non-null parameter values.
}
/// @nodoc
class _$SubmittedImpl implements _Submitted {
const _$SubmittedImpl();
@override
String toString() {
return 'LogoutFormEvent.submitted()';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType && other is _$SubmittedImpl);
}
@override
int get hashCode => runtimeType.hashCode;
@override
@optionalTypeArgs
TResult when<TResult extends Object?>({
required TResult Function() submitted,
}) {
return submitted();
}
@override
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult? Function()? submitted,
}) {
return submitted?.call();
}
@override
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
TResult Function()? submitted,
required TResult orElse(),
}) {
if (submitted != null) {
return submitted();
}
return orElse();
}
@override
@optionalTypeArgs
TResult map<TResult extends Object?>({
required TResult Function(_Submitted value) submitted,
}) {
return submitted(this);
}
@override
@optionalTypeArgs
TResult? mapOrNull<TResult extends Object?>({
TResult? Function(_Submitted value)? submitted,
}) {
return submitted?.call(this);
}
@override
@optionalTypeArgs
TResult maybeMap<TResult extends Object?>({
TResult Function(_Submitted value)? submitted,
required TResult orElse(),
}) {
if (submitted != null) {
return submitted(this);
}
return orElse();
}
}
abstract class _Submitted implements LogoutFormEvent {
const factory _Submitted() = _$SubmittedImpl;
}
/// @nodoc
mixin _$LogoutFormState {
Option<Either<AuthFailure, Unit>> get failureOrAuthOption =>
throw _privateConstructorUsedError;
bool get isSubmitting => throw _privateConstructorUsedError;
/// Create a copy of LogoutFormState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$LogoutFormStateCopyWith<LogoutFormState> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $LogoutFormStateCopyWith<$Res> {
factory $LogoutFormStateCopyWith(
LogoutFormState value,
$Res Function(LogoutFormState) then,
) = _$LogoutFormStateCopyWithImpl<$Res, LogoutFormState>;
@useResult
$Res call({
Option<Either<AuthFailure, Unit>> failureOrAuthOption,
bool isSubmitting,
});
}
/// @nodoc
class _$LogoutFormStateCopyWithImpl<$Res, $Val extends LogoutFormState>
implements $LogoutFormStateCopyWith<$Res> {
_$LogoutFormStateCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of LogoutFormState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({Object? failureOrAuthOption = null, Object? isSubmitting = null}) {
return _then(
_value.copyWith(
failureOrAuthOption: null == failureOrAuthOption
? _value.failureOrAuthOption
: failureOrAuthOption // ignore: cast_nullable_to_non_nullable
as Option<Either<AuthFailure, Unit>>,
isSubmitting: null == isSubmitting
? _value.isSubmitting
: isSubmitting // ignore: cast_nullable_to_non_nullable
as bool,
)
as $Val,
);
}
}
/// @nodoc
abstract class _$$LogoutFormStateImplCopyWith<$Res>
implements $LogoutFormStateCopyWith<$Res> {
factory _$$LogoutFormStateImplCopyWith(
_$LogoutFormStateImpl value,
$Res Function(_$LogoutFormStateImpl) then,
) = __$$LogoutFormStateImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({
Option<Either<AuthFailure, Unit>> failureOrAuthOption,
bool isSubmitting,
});
}
/// @nodoc
class __$$LogoutFormStateImplCopyWithImpl<$Res>
extends _$LogoutFormStateCopyWithImpl<$Res, _$LogoutFormStateImpl>
implements _$$LogoutFormStateImplCopyWith<$Res> {
__$$LogoutFormStateImplCopyWithImpl(
_$LogoutFormStateImpl _value,
$Res Function(_$LogoutFormStateImpl) _then,
) : super(_value, _then);
/// Create a copy of LogoutFormState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({Object? failureOrAuthOption = null, Object? isSubmitting = null}) {
return _then(
_$LogoutFormStateImpl(
failureOrAuthOption: null == failureOrAuthOption
? _value.failureOrAuthOption
: failureOrAuthOption // ignore: cast_nullable_to_non_nullable
as Option<Either<AuthFailure, Unit>>,
isSubmitting: null == isSubmitting
? _value.isSubmitting
: isSubmitting // ignore: cast_nullable_to_non_nullable
as bool,
),
);
}
}
/// @nodoc
class _$LogoutFormStateImpl implements _LogoutFormState {
const _$LogoutFormStateImpl({
required this.failureOrAuthOption,
this.isSubmitting = false,
});
@override
final Option<Either<AuthFailure, Unit>> failureOrAuthOption;
@override
@JsonKey()
final bool isSubmitting;
@override
String toString() {
return 'LogoutFormState(failureOrAuthOption: $failureOrAuthOption, isSubmitting: $isSubmitting)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$LogoutFormStateImpl &&
(identical(other.failureOrAuthOption, failureOrAuthOption) ||
other.failureOrAuthOption == failureOrAuthOption) &&
(identical(other.isSubmitting, isSubmitting) ||
other.isSubmitting == isSubmitting));
}
@override
int get hashCode =>
Object.hash(runtimeType, failureOrAuthOption, isSubmitting);
/// Create a copy of LogoutFormState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$LogoutFormStateImplCopyWith<_$LogoutFormStateImpl> get copyWith =>
__$$LogoutFormStateImplCopyWithImpl<_$LogoutFormStateImpl>(
this,
_$identity,
);
}
abstract class _LogoutFormState implements LogoutFormState {
const factory _LogoutFormState({
required final Option<Either<AuthFailure, Unit>> failureOrAuthOption,
final bool isSubmitting,
}) = _$LogoutFormStateImpl;
@override
Option<Either<AuthFailure, Unit>> get failureOrAuthOption;
@override
bool get isSubmitting;
/// Create a copy of LogoutFormState
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
_$$LogoutFormStateImplCopyWith<_$LogoutFormStateImpl> get copyWith =>
throw _privateConstructorUsedError;
}
@@ -0,0 +1,6 @@
part of 'logout_form_bloc.dart';
@freezed
class LogoutFormEvent with _$LogoutFormEvent {
const factory LogoutFormEvent.submitted() = _Submitted;
}
@@ -0,0 +1,12 @@
part of 'logout_form_bloc.dart';
@freezed
class LogoutFormState with _$LogoutFormState {
const factory LogoutFormState({
required Option<Either<AuthFailure, Unit>> failureOrAuthOption,
@Default(false) bool isSubmitting,
}) = _LogoutFormState;
factory LogoutFormState.initial() =>
LogoutFormState(failureOrAuthOption: none(), isSubmitting: false);
}
@@ -0,0 +1,42 @@
import 'package:bloc/bloc.dart';
import 'package:dartz/dartz.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:injectable/injectable.dart';
import '../../../domain/customer/customer.dart';
part 'customer_point_loader_event.dart';
part 'customer_point_loader_state.dart';
part 'customer_point_loader_bloc.freezed.dart';
@injectable
class CustomerPointLoaderBloc
extends Bloc<CustomerPointLoaderEvent, CustomerPointLoaderState> {
final ICustomerRepository _repository;
CustomerPointLoaderBloc(this._repository)
: super(CustomerPointLoaderState.initial()) {
on<CustomerPointLoaderEvent>(_onCustomerPointLoaderEvent);
}
Future<void> _onCustomerPointLoaderEvent(
CustomerPointLoaderEvent event,
Emitter<CustomerPointLoaderState> emit,
) {
return event.map(
fetched: (e) async {
emit(
state.copyWith(isFetching: true, failureOptionCustomerPoint: none()),
);
final result = await _repository.getPoints();
var data = result.fold(
(f) => state.copyWith(failureOptionCustomerPoint: optionOf(f)),
(customerPoint) => state.copyWith(customerPoint: customerPoint),
);
emit(data.copyWith(isFetching: false));
},
);
}
}
@@ -0,0 +1,391 @@
// 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 'customer_point_loader_bloc.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(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',
);
/// @nodoc
mixin _$CustomerPointLoaderEvent {
@optionalTypeArgs
TResult when<TResult extends Object?>({
required TResult Function() fetched,
}) => throw _privateConstructorUsedError;
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult? Function()? fetched,
}) => throw _privateConstructorUsedError;
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
TResult Function()? fetched,
required TResult orElse(),
}) => throw _privateConstructorUsedError;
@optionalTypeArgs
TResult map<TResult extends Object?>({
required TResult Function(_Fetched value) fetched,
}) => throw _privateConstructorUsedError;
@optionalTypeArgs
TResult? mapOrNull<TResult extends Object?>({
TResult? Function(_Fetched value)? fetched,
}) => throw _privateConstructorUsedError;
@optionalTypeArgs
TResult maybeMap<TResult extends Object?>({
TResult Function(_Fetched value)? fetched,
required TResult orElse(),
}) => throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $CustomerPointLoaderEventCopyWith<$Res> {
factory $CustomerPointLoaderEventCopyWith(
CustomerPointLoaderEvent value,
$Res Function(CustomerPointLoaderEvent) then,
) = _$CustomerPointLoaderEventCopyWithImpl<$Res, CustomerPointLoaderEvent>;
}
/// @nodoc
class _$CustomerPointLoaderEventCopyWithImpl<
$Res,
$Val extends CustomerPointLoaderEvent
>
implements $CustomerPointLoaderEventCopyWith<$Res> {
_$CustomerPointLoaderEventCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of CustomerPointLoaderEvent
/// with the given fields replaced by the non-null parameter values.
}
/// @nodoc
abstract class _$$FetchedImplCopyWith<$Res> {
factory _$$FetchedImplCopyWith(
_$FetchedImpl value,
$Res Function(_$FetchedImpl) then,
) = __$$FetchedImplCopyWithImpl<$Res>;
}
/// @nodoc
class __$$FetchedImplCopyWithImpl<$Res>
extends _$CustomerPointLoaderEventCopyWithImpl<$Res, _$FetchedImpl>
implements _$$FetchedImplCopyWith<$Res> {
__$$FetchedImplCopyWithImpl(
_$FetchedImpl _value,
$Res Function(_$FetchedImpl) _then,
) : super(_value, _then);
/// Create a copy of CustomerPointLoaderEvent
/// with the given fields replaced by the non-null parameter values.
}
/// @nodoc
class _$FetchedImpl implements _Fetched {
const _$FetchedImpl();
@override
String toString() {
return 'CustomerPointLoaderEvent.fetched()';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType && other is _$FetchedImpl);
}
@override
int get hashCode => runtimeType.hashCode;
@override
@optionalTypeArgs
TResult when<TResult extends Object?>({required TResult Function() fetched}) {
return fetched();
}
@override
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({TResult? Function()? fetched}) {
return fetched?.call();
}
@override
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
TResult Function()? fetched,
required TResult orElse(),
}) {
if (fetched != null) {
return fetched();
}
return orElse();
}
@override
@optionalTypeArgs
TResult map<TResult extends Object?>({
required TResult Function(_Fetched value) fetched,
}) {
return fetched(this);
}
@override
@optionalTypeArgs
TResult? mapOrNull<TResult extends Object?>({
TResult? Function(_Fetched value)? fetched,
}) {
return fetched?.call(this);
}
@override
@optionalTypeArgs
TResult maybeMap<TResult extends Object?>({
TResult Function(_Fetched value)? fetched,
required TResult orElse(),
}) {
if (fetched != null) {
return fetched(this);
}
return orElse();
}
}
abstract class _Fetched implements CustomerPointLoaderEvent {
const factory _Fetched() = _$FetchedImpl;
}
/// @nodoc
mixin _$CustomerPointLoaderState {
CustomerPoint get customerPoint => throw _privateConstructorUsedError;
Option<CustomerFailure> get failureOptionCustomerPoint =>
throw _privateConstructorUsedError;
bool get isFetching => throw _privateConstructorUsedError;
/// Create a copy of CustomerPointLoaderState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$CustomerPointLoaderStateCopyWith<CustomerPointLoaderState> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $CustomerPointLoaderStateCopyWith<$Res> {
factory $CustomerPointLoaderStateCopyWith(
CustomerPointLoaderState value,
$Res Function(CustomerPointLoaderState) then,
) = _$CustomerPointLoaderStateCopyWithImpl<$Res, CustomerPointLoaderState>;
@useResult
$Res call({
CustomerPoint customerPoint,
Option<CustomerFailure> failureOptionCustomerPoint,
bool isFetching,
});
$CustomerPointCopyWith<$Res> get customerPoint;
}
/// @nodoc
class _$CustomerPointLoaderStateCopyWithImpl<
$Res,
$Val extends CustomerPointLoaderState
>
implements $CustomerPointLoaderStateCopyWith<$Res> {
_$CustomerPointLoaderStateCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of CustomerPointLoaderState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? customerPoint = null,
Object? failureOptionCustomerPoint = null,
Object? isFetching = null,
}) {
return _then(
_value.copyWith(
customerPoint: null == customerPoint
? _value.customerPoint
: customerPoint // ignore: cast_nullable_to_non_nullable
as CustomerPoint,
failureOptionCustomerPoint: null == failureOptionCustomerPoint
? _value.failureOptionCustomerPoint
: failureOptionCustomerPoint // ignore: cast_nullable_to_non_nullable
as Option<CustomerFailure>,
isFetching: null == isFetching
? _value.isFetching
: isFetching // ignore: cast_nullable_to_non_nullable
as bool,
)
as $Val,
);
}
/// Create a copy of CustomerPointLoaderState
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$CustomerPointCopyWith<$Res> get customerPoint {
return $CustomerPointCopyWith<$Res>(_value.customerPoint, (value) {
return _then(_value.copyWith(customerPoint: value) as $Val);
});
}
}
/// @nodoc
abstract class _$$CustomerPointLoaderStateImplCopyWith<$Res>
implements $CustomerPointLoaderStateCopyWith<$Res> {
factory _$$CustomerPointLoaderStateImplCopyWith(
_$CustomerPointLoaderStateImpl value,
$Res Function(_$CustomerPointLoaderStateImpl) then,
) = __$$CustomerPointLoaderStateImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({
CustomerPoint customerPoint,
Option<CustomerFailure> failureOptionCustomerPoint,
bool isFetching,
});
@override
$CustomerPointCopyWith<$Res> get customerPoint;
}
/// @nodoc
class __$$CustomerPointLoaderStateImplCopyWithImpl<$Res>
extends
_$CustomerPointLoaderStateCopyWithImpl<
$Res,
_$CustomerPointLoaderStateImpl
>
implements _$$CustomerPointLoaderStateImplCopyWith<$Res> {
__$$CustomerPointLoaderStateImplCopyWithImpl(
_$CustomerPointLoaderStateImpl _value,
$Res Function(_$CustomerPointLoaderStateImpl) _then,
) : super(_value, _then);
/// Create a copy of CustomerPointLoaderState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? customerPoint = null,
Object? failureOptionCustomerPoint = null,
Object? isFetching = null,
}) {
return _then(
_$CustomerPointLoaderStateImpl(
customerPoint: null == customerPoint
? _value.customerPoint
: customerPoint // ignore: cast_nullable_to_non_nullable
as CustomerPoint,
failureOptionCustomerPoint: null == failureOptionCustomerPoint
? _value.failureOptionCustomerPoint
: failureOptionCustomerPoint // ignore: cast_nullable_to_non_nullable
as Option<CustomerFailure>,
isFetching: null == isFetching
? _value.isFetching
: isFetching // ignore: cast_nullable_to_non_nullable
as bool,
),
);
}
}
/// @nodoc
class _$CustomerPointLoaderStateImpl implements _CustomerPointLoaderState {
const _$CustomerPointLoaderStateImpl({
required this.customerPoint,
required this.failureOptionCustomerPoint,
this.isFetching = false,
});
@override
final CustomerPoint customerPoint;
@override
final Option<CustomerFailure> failureOptionCustomerPoint;
@override
@JsonKey()
final bool isFetching;
@override
String toString() {
return 'CustomerPointLoaderState(customerPoint: $customerPoint, failureOptionCustomerPoint: $failureOptionCustomerPoint, isFetching: $isFetching)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$CustomerPointLoaderStateImpl &&
(identical(other.customerPoint, customerPoint) ||
other.customerPoint == customerPoint) &&
(identical(
other.failureOptionCustomerPoint,
failureOptionCustomerPoint,
) ||
other.failureOptionCustomerPoint ==
failureOptionCustomerPoint) &&
(identical(other.isFetching, isFetching) ||
other.isFetching == isFetching));
}
@override
int get hashCode => Object.hash(
runtimeType,
customerPoint,
failureOptionCustomerPoint,
isFetching,
);
/// Create a copy of CustomerPointLoaderState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$CustomerPointLoaderStateImplCopyWith<_$CustomerPointLoaderStateImpl>
get copyWith =>
__$$CustomerPointLoaderStateImplCopyWithImpl<
_$CustomerPointLoaderStateImpl
>(this, _$identity);
}
abstract class _CustomerPointLoaderState implements CustomerPointLoaderState {
const factory _CustomerPointLoaderState({
required final CustomerPoint customerPoint,
required final Option<CustomerFailure> failureOptionCustomerPoint,
final bool isFetching,
}) = _$CustomerPointLoaderStateImpl;
@override
CustomerPoint get customerPoint;
@override
Option<CustomerFailure> get failureOptionCustomerPoint;
@override
bool get isFetching;
/// Create a copy of CustomerPointLoaderState
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
_$$CustomerPointLoaderStateImplCopyWith<_$CustomerPointLoaderStateImpl>
get copyWith => throw _privateConstructorUsedError;
}
@@ -0,0 +1,6 @@
part of 'customer_point_loader_bloc.dart';
@freezed
class CustomerPointLoaderEvent with _$CustomerPointLoaderEvent {
const factory CustomerPointLoaderEvent.fetched() = _Fetched;
}
@@ -0,0 +1,15 @@
part of 'customer_point_loader_bloc.dart';
@freezed
class CustomerPointLoaderState with _$CustomerPointLoaderState {
const factory CustomerPointLoaderState({
required CustomerPoint customerPoint,
required Option<CustomerFailure> failureOptionCustomerPoint,
@Default(false) bool isFetching,
}) = _CustomerPointLoaderState;
factory CustomerPointLoaderState.initial() => CustomerPointLoaderState(
customerPoint: CustomerPoint.empty(),
failureOptionCustomerPoint: none(),
);
}
@@ -0,0 +1,42 @@
import 'package:bloc/bloc.dart';
import 'package:dartz/dartz.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:injectable/injectable.dart';
import '../../../domain/game/game.dart';
part 'ferris_wheel_loader_event.dart';
part 'ferris_wheel_loader_state.dart';
part 'ferris_wheel_loader_bloc.freezed.dart';
@injectable
class FerrisWheelLoaderBloc
extends Bloc<FerrisWheelLoaderEvent, FerrisWheelLoaderState> {
final IGameRepository _repository;
FerrisWheelLoaderBloc(this._repository)
: super(FerrisWheelLoaderState.initial()) {
on<FerrisWheelLoaderEvent>(_onFerrisWheelLoaderEvent);
}
Future<void> _onFerrisWheelLoaderEvent(
FerrisWheelLoaderEvent event,
Emitter<FerrisWheelLoaderState> emit,
) {
return event.map(
fetched: (e) async {
emit(
state.copyWith(isFetching: true, failureOptionFerrisWheel: none()),
);
final result = await _repository.ferrisWheel();
var data = result.fold(
(f) => state.copyWith(failureOptionFerrisWheel: optionOf(f)),
(ferrisWheel) => state.copyWith(ferrisWheel: ferrisWheel),
);
emit(data.copyWith(isFetching: false));
},
);
}
}
@@ -0,0 +1,388 @@
// 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 'ferris_wheel_loader_bloc.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(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',
);
/// @nodoc
mixin _$FerrisWheelLoaderEvent {
@optionalTypeArgs
TResult when<TResult extends Object?>({
required TResult Function() fetched,
}) => throw _privateConstructorUsedError;
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult? Function()? fetched,
}) => throw _privateConstructorUsedError;
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
TResult Function()? fetched,
required TResult orElse(),
}) => throw _privateConstructorUsedError;
@optionalTypeArgs
TResult map<TResult extends Object?>({
required TResult Function(_Fetched value) fetched,
}) => throw _privateConstructorUsedError;
@optionalTypeArgs
TResult? mapOrNull<TResult extends Object?>({
TResult? Function(_Fetched value)? fetched,
}) => throw _privateConstructorUsedError;
@optionalTypeArgs
TResult maybeMap<TResult extends Object?>({
TResult Function(_Fetched value)? fetched,
required TResult orElse(),
}) => throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $FerrisWheelLoaderEventCopyWith<$Res> {
factory $FerrisWheelLoaderEventCopyWith(
FerrisWheelLoaderEvent value,
$Res Function(FerrisWheelLoaderEvent) then,
) = _$FerrisWheelLoaderEventCopyWithImpl<$Res, FerrisWheelLoaderEvent>;
}
/// @nodoc
class _$FerrisWheelLoaderEventCopyWithImpl<
$Res,
$Val extends FerrisWheelLoaderEvent
>
implements $FerrisWheelLoaderEventCopyWith<$Res> {
_$FerrisWheelLoaderEventCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of FerrisWheelLoaderEvent
/// with the given fields replaced by the non-null parameter values.
}
/// @nodoc
abstract class _$$FetchedImplCopyWith<$Res> {
factory _$$FetchedImplCopyWith(
_$FetchedImpl value,
$Res Function(_$FetchedImpl) then,
) = __$$FetchedImplCopyWithImpl<$Res>;
}
/// @nodoc
class __$$FetchedImplCopyWithImpl<$Res>
extends _$FerrisWheelLoaderEventCopyWithImpl<$Res, _$FetchedImpl>
implements _$$FetchedImplCopyWith<$Res> {
__$$FetchedImplCopyWithImpl(
_$FetchedImpl _value,
$Res Function(_$FetchedImpl) _then,
) : super(_value, _then);
/// Create a copy of FerrisWheelLoaderEvent
/// with the given fields replaced by the non-null parameter values.
}
/// @nodoc
class _$FetchedImpl implements _Fetched {
const _$FetchedImpl();
@override
String toString() {
return 'FerrisWheelLoaderEvent.fetched()';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType && other is _$FetchedImpl);
}
@override
int get hashCode => runtimeType.hashCode;
@override
@optionalTypeArgs
TResult when<TResult extends Object?>({required TResult Function() fetched}) {
return fetched();
}
@override
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({TResult? Function()? fetched}) {
return fetched?.call();
}
@override
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
TResult Function()? fetched,
required TResult orElse(),
}) {
if (fetched != null) {
return fetched();
}
return orElse();
}
@override
@optionalTypeArgs
TResult map<TResult extends Object?>({
required TResult Function(_Fetched value) fetched,
}) {
return fetched(this);
}
@override
@optionalTypeArgs
TResult? mapOrNull<TResult extends Object?>({
TResult? Function(_Fetched value)? fetched,
}) {
return fetched?.call(this);
}
@override
@optionalTypeArgs
TResult maybeMap<TResult extends Object?>({
TResult Function(_Fetched value)? fetched,
required TResult orElse(),
}) {
if (fetched != null) {
return fetched(this);
}
return orElse();
}
}
abstract class _Fetched implements FerrisWheelLoaderEvent {
const factory _Fetched() = _$FetchedImpl;
}
/// @nodoc
mixin _$FerrisWheelLoaderState {
Game get ferrisWheel => throw _privateConstructorUsedError;
Option<GameFailure> get failureOptionFerrisWheel =>
throw _privateConstructorUsedError;
bool get isFetching => throw _privateConstructorUsedError;
/// Create a copy of FerrisWheelLoaderState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$FerrisWheelLoaderStateCopyWith<FerrisWheelLoaderState> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $FerrisWheelLoaderStateCopyWith<$Res> {
factory $FerrisWheelLoaderStateCopyWith(
FerrisWheelLoaderState value,
$Res Function(FerrisWheelLoaderState) then,
) = _$FerrisWheelLoaderStateCopyWithImpl<$Res, FerrisWheelLoaderState>;
@useResult
$Res call({
Game ferrisWheel,
Option<GameFailure> failureOptionFerrisWheel,
bool isFetching,
});
$GameCopyWith<$Res> get ferrisWheel;
}
/// @nodoc
class _$FerrisWheelLoaderStateCopyWithImpl<
$Res,
$Val extends FerrisWheelLoaderState
>
implements $FerrisWheelLoaderStateCopyWith<$Res> {
_$FerrisWheelLoaderStateCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of FerrisWheelLoaderState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? ferrisWheel = null,
Object? failureOptionFerrisWheel = null,
Object? isFetching = null,
}) {
return _then(
_value.copyWith(
ferrisWheel: null == ferrisWheel
? _value.ferrisWheel
: ferrisWheel // ignore: cast_nullable_to_non_nullable
as Game,
failureOptionFerrisWheel: null == failureOptionFerrisWheel
? _value.failureOptionFerrisWheel
: failureOptionFerrisWheel // ignore: cast_nullable_to_non_nullable
as Option<GameFailure>,
isFetching: null == isFetching
? _value.isFetching
: isFetching // ignore: cast_nullable_to_non_nullable
as bool,
)
as $Val,
);
}
/// Create a copy of FerrisWheelLoaderState
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$GameCopyWith<$Res> get ferrisWheel {
return $GameCopyWith<$Res>(_value.ferrisWheel, (value) {
return _then(_value.copyWith(ferrisWheel: value) as $Val);
});
}
}
/// @nodoc
abstract class _$$FerrisWheelLoaderStateImplCopyWith<$Res>
implements $FerrisWheelLoaderStateCopyWith<$Res> {
factory _$$FerrisWheelLoaderStateImplCopyWith(
_$FerrisWheelLoaderStateImpl value,
$Res Function(_$FerrisWheelLoaderStateImpl) then,
) = __$$FerrisWheelLoaderStateImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({
Game ferrisWheel,
Option<GameFailure> failureOptionFerrisWheel,
bool isFetching,
});
@override
$GameCopyWith<$Res> get ferrisWheel;
}
/// @nodoc
class __$$FerrisWheelLoaderStateImplCopyWithImpl<$Res>
extends
_$FerrisWheelLoaderStateCopyWithImpl<$Res, _$FerrisWheelLoaderStateImpl>
implements _$$FerrisWheelLoaderStateImplCopyWith<$Res> {
__$$FerrisWheelLoaderStateImplCopyWithImpl(
_$FerrisWheelLoaderStateImpl _value,
$Res Function(_$FerrisWheelLoaderStateImpl) _then,
) : super(_value, _then);
/// Create a copy of FerrisWheelLoaderState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? ferrisWheel = null,
Object? failureOptionFerrisWheel = null,
Object? isFetching = null,
}) {
return _then(
_$FerrisWheelLoaderStateImpl(
ferrisWheel: null == ferrisWheel
? _value.ferrisWheel
: ferrisWheel // ignore: cast_nullable_to_non_nullable
as Game,
failureOptionFerrisWheel: null == failureOptionFerrisWheel
? _value.failureOptionFerrisWheel
: failureOptionFerrisWheel // ignore: cast_nullable_to_non_nullable
as Option<GameFailure>,
isFetching: null == isFetching
? _value.isFetching
: isFetching // ignore: cast_nullable_to_non_nullable
as bool,
),
);
}
}
/// @nodoc
class _$FerrisWheelLoaderStateImpl implements _FerrisWheelLoaderState {
const _$FerrisWheelLoaderStateImpl({
required this.ferrisWheel,
required this.failureOptionFerrisWheel,
this.isFetching = false,
});
@override
final Game ferrisWheel;
@override
final Option<GameFailure> failureOptionFerrisWheel;
@override
@JsonKey()
final bool isFetching;
@override
String toString() {
return 'FerrisWheelLoaderState(ferrisWheel: $ferrisWheel, failureOptionFerrisWheel: $failureOptionFerrisWheel, isFetching: $isFetching)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$FerrisWheelLoaderStateImpl &&
(identical(other.ferrisWheel, ferrisWheel) ||
other.ferrisWheel == ferrisWheel) &&
(identical(
other.failureOptionFerrisWheel,
failureOptionFerrisWheel,
) ||
other.failureOptionFerrisWheel == failureOptionFerrisWheel) &&
(identical(other.isFetching, isFetching) ||
other.isFetching == isFetching));
}
@override
int get hashCode => Object.hash(
runtimeType,
ferrisWheel,
failureOptionFerrisWheel,
isFetching,
);
/// Create a copy of FerrisWheelLoaderState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$FerrisWheelLoaderStateImplCopyWith<_$FerrisWheelLoaderStateImpl>
get copyWith =>
__$$FerrisWheelLoaderStateImplCopyWithImpl<_$FerrisWheelLoaderStateImpl>(
this,
_$identity,
);
}
abstract class _FerrisWheelLoaderState implements FerrisWheelLoaderState {
const factory _FerrisWheelLoaderState({
required final Game ferrisWheel,
required final Option<GameFailure> failureOptionFerrisWheel,
final bool isFetching,
}) = _$FerrisWheelLoaderStateImpl;
@override
Game get ferrisWheel;
@override
Option<GameFailure> get failureOptionFerrisWheel;
@override
bool get isFetching;
/// Create a copy of FerrisWheelLoaderState
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
_$$FerrisWheelLoaderStateImplCopyWith<_$FerrisWheelLoaderStateImpl>
get copyWith => throw _privateConstructorUsedError;
}
@@ -0,0 +1,6 @@
part of 'ferris_wheel_loader_bloc.dart';
@freezed
class FerrisWheelLoaderEvent with _$FerrisWheelLoaderEvent {
const factory FerrisWheelLoaderEvent.fetched() = _Fetched;
}
@@ -0,0 +1,15 @@
part of 'ferris_wheel_loader_bloc.dart';
@freezed
class FerrisWheelLoaderState with _$FerrisWheelLoaderState {
const factory FerrisWheelLoaderState({
required Game ferrisWheel,
required Option<GameFailure> failureOptionFerrisWheel,
@Default(false) bool isFetching,
}) = _FerrisWheelLoaderState;
factory FerrisWheelLoaderState.initial() => FerrisWheelLoaderState(
ferrisWheel: Game.empty(),
failureOptionFerrisWheel: none(),
);
}
@@ -1,15 +1,40 @@
import 'package:dio/dio.dart';
import 'dart:developer';
import 'package:auto_route/auto_route.dart';
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../../../presentation/router/app_router.gr.dart';
import '../../constant/local_storage_key.dart';
import '../errors/unauthorized_error.dart';
class UnauthorizedInterceptor extends Interceptor {
static final GlobalKey<NavigatorState> navigatorKey =
GlobalKey<NavigatorState>();
@override
void onError(DioException err, ErrorInterceptorHandler handler) {
void onError(DioException err, ErrorInterceptorHandler handler) async {
if (err.response?.statusCode == 401 ||
err.response?.statusCode == 403 ||
err.response?.statusCode == 419) {
await _handleTokenExpired();
return super.onError(UnauthorizedError(err, null), handler);
}
super.onError(err, handler);
}
Future<void> _handleTokenExpired() async {
// Clear stored token
final prefs = await SharedPreferences.getInstance();
await prefs.remove(LocalStorageKey.token);
await prefs.remove(LocalStorageKey.user);
await prefs.clear(); // Optional: clear all user data
log('handleTokenExpired');
// Navigate to login page
final context = navigatorKey.currentContext;
if (context != null) {
// Option 1: Navigate and remove all previous routes
context.router.replaceAll([LoginRoute()]);
}
}
}
+7 -1
View File
@@ -1,3 +1,9 @@
import '../../sample/sample_data.dart';
class AppConstant {
static const String appName = "";
static const String appName = "Enaklo";
static const String coinName = "EnakCoin";
static const String poinName = "EnakPoin";
}
MerchantModel merchant = merchants.first;
+26
View File
@@ -0,0 +1,26 @@
import '../../presentation/components/assets/assets.gen.dart';
class Service {
Service({
required this.name,
required this.description,
required this.imagePath,
});
final String name;
final String imagePath;
final String description;
}
List<Service> services = [
Service(
name: 'Dine In',
description: 'Makan langsung di tempat',
imagePath: Assets.icons.dineIn.path,
),
Service(
name: 'Take Away',
description: 'Pesan dan bawa pulang',
imagePath: Assets.icons.takeaway.path,
),
];
@@ -0,0 +1,28 @@
part of 'extension.dart';
extension DoubleExt on double {
String get currencyFormatRpV2 => NumberFormat.currency(
locale: 'id',
symbol: 'Rp ',
decimalDigits: 0,
).format(this);
}
extension StringX on String {
String get currencyFormatRp {
final parsedValue = int.tryParse(this) ?? 0;
return NumberFormat.currency(
locale: 'id',
symbol: 'Rp ',
decimalDigits: 0,
).format(parsedValue);
}
}
extension IntegerExt on int {
String get currencyFormatRp => NumberFormat.currency(
locale: 'id',
symbol: 'Rp ',
decimalDigits: 0,
).format(this);
}
+1
View File
@@ -3,6 +3,7 @@ import 'package:intl/intl.dart';
import '../../domain/auth/auth.dart';
part 'date_extension.dart';
part 'currency_extension.dart';
extension StringExt on String {
CheckPhoneStatus toCheckPhoneStatus() {
+11
View File
@@ -1,4 +1,8 @@
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../../injection.dart';
import '../constant/local_storage_key.dart';
void dismissKeyboard(BuildContext context) {
final currentFocus = FocusScope.of(context);
@@ -13,3 +17,10 @@ String getNormalizePhone(String phoneNumber) {
: phoneNumber;
return '62$normalizedPhone';
}
Map<String, dynamic> getAuthorizationHeader() {
return {
'Authorization':
'Bearer ${getIt<SharedPreferences>().getString(LocalStorageKey.token)}',
};
}
+56 -92
View File
@@ -1,19 +1,21 @@
// wheel_painter.dart - Fixed implementation with consistent positioning
import 'dart:math' as math;
import 'package:flutter/material.dart';
import '../../presentation/pages/mini_games/ferris_wheel/data/model.dart';
import '../theme/theme.dart';
import '../../domain/game/game.dart';
class WheelPainter extends CustomPainter {
final List<WheelSection> sections;
final List<GamePrize> gamePrizes;
final Color Function(GamePrize prize, int index) getPrizeColor;
WheelPainter({required this.sections});
WheelPainter({required this.gamePrizes, required this.getPrizeColor});
@override
void paint(Canvas canvas, Size size) {
if (gamePrizes.isEmpty) return;
final center = Offset(size.width / 2, size.height / 2);
final radius = size.width / 2;
final sectionAngle = 2 * math.pi / sections.length;
final sectionAngle = 2 * math.pi / gamePrizes.length;
// Draw outer white border
final outerBorderPaint = Paint()
@@ -33,13 +35,15 @@ class WheelPainter extends CustomPainter {
..style = PaintingStyle.fill;
canvas.drawCircle(center, radius - 20, innerWhitePaint);
// Draw sections
for (int i = 0; i < sections.length; i++) {
final startAngle = i * sectionAngle - math.pi / 2;
// Draw sections - KONSISTEN dengan logic spin
for (int i = 0; i < gamePrizes.length; i++) {
final prize = gamePrizes[i];
// Section 0 di top (-π/2), section 1 di kanan atas, dst (clockwise)
final startAngle = (-math.pi / 2) + (i * sectionAngle);
// Section background
final sectionPaint = Paint()
..color = sections[i].color
..color = getPrizeColor(prize, i)
..style = PaintingStyle.fill;
canvas.drawArc(
@@ -50,82 +54,67 @@ class WheelPainter extends CustomPainter {
sectionPaint,
);
// Draw icon in each section
final iconAngle = startAngle + sectionAngle / 2;
final iconPosition = Offset(
center.dx + (radius - 60) * math.cos(iconAngle),
center.dy + (radius - 60) * math.sin(iconAngle),
);
// Draw icon background circle
final iconBgPaint = Paint()
..color = Colors.white
..style = PaintingStyle.fill;
canvas.drawCircle(iconPosition, 16, iconBgPaint);
// Save canvas state for icon drawing
canvas.save();
canvas.translate(iconPosition.dx, iconPosition.dy);
// Draw icon using TextPainter to simulate Icon widget
final iconText = _getIconText(sections[i].icon);
final iconTextPainter = TextPainter(
text: TextSpan(
text: iconText,
style: TextStyle(
fontFamily: 'MaterialIcons',
fontSize: 20,
color: sections[i].color,
fontWeight: FontWeight.normal,
),
),
textDirection: TextDirection.ltr,
);
iconTextPainter.layout();
iconTextPainter.paint(
canvas,
Offset(-iconTextPainter.width / 2, -iconTextPainter.height / 2),
);
canvas.restore();
// Draw prize text
final textAngle = startAngle + sectionAngle / 2;
final textPosition = Offset(
center.dx + (radius - 100) * math.cos(textAngle),
center.dy + (radius - 100) * math.sin(textAngle),
center.dx + (radius - 80) * math.cos(textAngle),
center.dy + (radius - 80) * math.sin(textAngle),
);
// Save canvas state for text rotation
canvas.save();
canvas.translate(textPosition.dx, textPosition.dy);
canvas.rotate(textAngle + math.pi / 2);
final textPainter = TextPainter(
text: TextSpan(
text: sections[i].prize,
text: prize.name,
style: const TextStyle(
color: Colors.white,
fontSize: 10,
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
textDirection: TextDirection.ltr,
textAlign: TextAlign.center,
);
textPainter.layout(maxWidth: 100);
textPainter.paint(
canvas,
Offset(-textPainter.width / 2, -textPainter.height / 2),
);
canvas.restore();
// DEBUG: Draw section number
final numberPosition = Offset(
center.dx + (radius - 50) * math.cos(textAngle),
center.dy + (radius - 50) * math.sin(textAngle),
);
canvas.drawCircle(numberPosition, 15, Paint()..color = Colors.white);
final numberPainter = TextPainter(
text: TextSpan(
text: i.toString(),
style: const TextStyle(
color: Colors.black,
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
textDirection: TextDirection.ltr,
);
textPainter.layout();
textPainter.paint(
numberPainter.layout();
numberPainter.paint(
canvas,
Offset(-textPainter.width / 2, -textPainter.height / 2),
Offset(
numberPosition.dx - numberPainter.width / 2,
numberPosition.dy - numberPainter.height / 2,
),
);
canvas.restore();
}
// Draw white dots around the outer edge
// Draw white dots
final dotPaint = Paint()
..color = Colors.white
..style = PaintingStyle.fill;
for (int i = 0; i < 24; i++) {
final dotAngle = (2 * math.pi / 24) * i;
final dotPosition = Offset(
@@ -137,15 +126,14 @@ class WheelPainter extends CustomPainter {
// Draw section dividers
final dividerPaint = Paint()
..color = AppColor.white
..color = Colors.white
..style = PaintingStyle.stroke
..strokeWidth = 2;
for (int i = 0; i < sections.length; i++) {
final angle = i * sectionAngle - math.pi / 2;
for (int i = 0; i < gamePrizes.length; i++) {
final angle = (-math.pi / 2) + (i * sectionAngle);
final lineStart = Offset(
center.dx + (radius - 130) * math.cos(angle),
center.dy + (radius - 130) * math.sin(angle),
center.dx + (radius - 110) * math.cos(angle),
center.dy + (radius - 110) * math.sin(angle),
);
final lineEnd = Offset(
center.dx + (radius - 22) * math.cos(angle),
@@ -155,30 +143,6 @@ class WheelPainter extends CustomPainter {
}
}
String _getIconText(IconData icon) {
// Convert IconData to Unicode string for drawing
switch (icon.codePoint) {
case 0xe8f4: // Icons.visibility
return String.fromCharCode(0xe8f4);
case 0xe8f5: // Icons.visibility_off
return String.fromCharCode(0xe8f5);
case 0xe850: // Icons.account_balance_wallet
return String.fromCharCode(0xe850);
case 0xe151: // Icons.card_giftcard
return String.fromCharCode(0xe151);
case 0xe5d5: // Icons.refresh
return String.fromCharCode(0xe5d5);
case 0xe263: // Icons.attach_money
return String.fromCharCode(0xe263);
case 0xe8a1: // Icons.redeem
return String.fromCharCode(0xe8a1);
case 0xe57d: // Icons.monetization_on
return String.fromCharCode(0xe57d);
default:
return String.fromCharCode(0xe87c); // Default star icon
}
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
}
+5 -1
View File
@@ -1,3 +1,7 @@
part of 'theme.dart';
class AppValue {}
class AppValue {
static const double padding = 16;
static const double margin = 16;
static const double borderRadius = 12;
}
+13 -2
View File
@@ -43,7 +43,7 @@ class ThemeApp {
foregroundColor: AppColor.textPrimary,
elevation: 0,
titleTextStyle: AppStyle.xl.copyWith(
color: AppColor.primary,
color: AppColor.textPrimary,
fontWeight: FontWeight.w600,
),
centerTitle: true,
@@ -55,7 +55,18 @@ class ThemeApp {
backgroundColor: AppColor.primary,
foregroundColor: Colors.white,
elevation: 0,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppValue.borderRadius),
),
),
),
outlinedButtonTheme: OutlinedButtonThemeData(
style: OutlinedButton.styleFrom(
foregroundColor: AppColor.primary,
shape: RoundedRectangleBorder(
side: BorderSide(color: AppColor.border),
borderRadius: BorderRadiusGeometry.circular(AppValue.borderRadius),
),
),
),
inputDecorationTheme: InputDecorationTheme(
+6
View File
@@ -5,4 +5,10 @@ class ApiPath {
static String setPassword = '/api/v1/customer-auth/register/set-password';
static String login = '/api/v1/customer-auth/login';
static String resend = '/api/v1/customer-auth/resend-otp';
// Marketing
static String ferrisWheel = '/api/v1/customer/ferris-wheel';
// Customer
static String customerPoint = '/api/v1/customer/points';
}
+10
View File
@@ -0,0 +1,10 @@
import 'package:dartz/dartz.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import '../../common/api/api_failure.dart';
part 'customer.freezed.dart';
part 'entities/customer_point_entity.dart';
part 'failures/customer_failures.dart';
part 'repositories/i_customer_repository.dart';
+713
View File
@@ -0,0 +1,713 @@
// 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 'customer.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(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',
);
/// @nodoc
mixin _$CustomerPoint {
String get status => throw _privateConstructorUsedError;
String get message => throw _privateConstructorUsedError;
int get totalPoints => throw _privateConstructorUsedError;
String get lastUpdated => throw _privateConstructorUsedError;
/// Create a copy of CustomerPoint
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$CustomerPointCopyWith<CustomerPoint> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $CustomerPointCopyWith<$Res> {
factory $CustomerPointCopyWith(
CustomerPoint value,
$Res Function(CustomerPoint) then,
) = _$CustomerPointCopyWithImpl<$Res, CustomerPoint>;
@useResult
$Res call({
String status,
String message,
int totalPoints,
String lastUpdated,
});
}
/// @nodoc
class _$CustomerPointCopyWithImpl<$Res, $Val extends CustomerPoint>
implements $CustomerPointCopyWith<$Res> {
_$CustomerPointCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of CustomerPoint
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? status = null,
Object? message = null,
Object? totalPoints = null,
Object? lastUpdated = null,
}) {
return _then(
_value.copyWith(
status: null == status
? _value.status
: status // ignore: cast_nullable_to_non_nullable
as String,
message: null == message
? _value.message
: message // ignore: cast_nullable_to_non_nullable
as String,
totalPoints: null == totalPoints
? _value.totalPoints
: totalPoints // ignore: cast_nullable_to_non_nullable
as int,
lastUpdated: null == lastUpdated
? _value.lastUpdated
: lastUpdated // ignore: cast_nullable_to_non_nullable
as String,
)
as $Val,
);
}
}
/// @nodoc
abstract class _$$CustomerPointImplCopyWith<$Res>
implements $CustomerPointCopyWith<$Res> {
factory _$$CustomerPointImplCopyWith(
_$CustomerPointImpl value,
$Res Function(_$CustomerPointImpl) then,
) = __$$CustomerPointImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({
String status,
String message,
int totalPoints,
String lastUpdated,
});
}
/// @nodoc
class __$$CustomerPointImplCopyWithImpl<$Res>
extends _$CustomerPointCopyWithImpl<$Res, _$CustomerPointImpl>
implements _$$CustomerPointImplCopyWith<$Res> {
__$$CustomerPointImplCopyWithImpl(
_$CustomerPointImpl _value,
$Res Function(_$CustomerPointImpl) _then,
) : super(_value, _then);
/// Create a copy of CustomerPoint
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? status = null,
Object? message = null,
Object? totalPoints = null,
Object? lastUpdated = null,
}) {
return _then(
_$CustomerPointImpl(
status: null == status
? _value.status
: status // ignore: cast_nullable_to_non_nullable
as String,
message: null == message
? _value.message
: message // ignore: cast_nullable_to_non_nullable
as String,
totalPoints: null == totalPoints
? _value.totalPoints
: totalPoints // ignore: cast_nullable_to_non_nullable
as int,
lastUpdated: null == lastUpdated
? _value.lastUpdated
: lastUpdated // ignore: cast_nullable_to_non_nullable
as String,
),
);
}
}
/// @nodoc
class _$CustomerPointImpl implements _CustomerPoint {
const _$CustomerPointImpl({
required this.status,
required this.message,
required this.totalPoints,
required this.lastUpdated,
});
@override
final String status;
@override
final String message;
@override
final int totalPoints;
@override
final String lastUpdated;
@override
String toString() {
return 'CustomerPoint(status: $status, message: $message, totalPoints: $totalPoints, lastUpdated: $lastUpdated)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$CustomerPointImpl &&
(identical(other.status, status) || other.status == status) &&
(identical(other.message, message) || other.message == message) &&
(identical(other.totalPoints, totalPoints) ||
other.totalPoints == totalPoints) &&
(identical(other.lastUpdated, lastUpdated) ||
other.lastUpdated == lastUpdated));
}
@override
int get hashCode =>
Object.hash(runtimeType, status, message, totalPoints, lastUpdated);
/// Create a copy of CustomerPoint
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$CustomerPointImplCopyWith<_$CustomerPointImpl> get copyWith =>
__$$CustomerPointImplCopyWithImpl<_$CustomerPointImpl>(this, _$identity);
}
abstract class _CustomerPoint implements CustomerPoint {
const factory _CustomerPoint({
required final String status,
required final String message,
required final int totalPoints,
required final String lastUpdated,
}) = _$CustomerPointImpl;
@override
String get status;
@override
String get message;
@override
int get totalPoints;
@override
String get lastUpdated;
/// Create a copy of CustomerPoint
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
_$$CustomerPointImplCopyWith<_$CustomerPointImpl> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
mixin _$CustomerFailure {
@optionalTypeArgs
TResult when<TResult extends Object?>({
required TResult Function(ApiFailure failure) serverError,
required TResult Function() unexpectedError,
required TResult Function(String erroMessage) dynamicErrorMessage,
}) => throw _privateConstructorUsedError;
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult? Function(ApiFailure failure)? serverError,
TResult? Function()? unexpectedError,
TResult? Function(String erroMessage)? dynamicErrorMessage,
}) => throw _privateConstructorUsedError;
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
TResult Function(ApiFailure failure)? serverError,
TResult Function()? unexpectedError,
TResult Function(String erroMessage)? dynamicErrorMessage,
required TResult orElse(),
}) => throw _privateConstructorUsedError;
@optionalTypeArgs
TResult map<TResult extends Object?>({
required TResult Function(_ServerError value) serverError,
required TResult Function(_UnexpectedError value) unexpectedError,
required TResult Function(_DynamicErrorMessage value) dynamicErrorMessage,
}) => throw _privateConstructorUsedError;
@optionalTypeArgs
TResult? mapOrNull<TResult extends Object?>({
TResult? Function(_ServerError value)? serverError,
TResult? Function(_UnexpectedError value)? unexpectedError,
TResult? Function(_DynamicErrorMessage value)? dynamicErrorMessage,
}) => throw _privateConstructorUsedError;
@optionalTypeArgs
TResult maybeMap<TResult extends Object?>({
TResult Function(_ServerError value)? serverError,
TResult Function(_UnexpectedError value)? unexpectedError,
TResult Function(_DynamicErrorMessage value)? dynamicErrorMessage,
required TResult orElse(),
}) => throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $CustomerFailureCopyWith<$Res> {
factory $CustomerFailureCopyWith(
CustomerFailure value,
$Res Function(CustomerFailure) then,
) = _$CustomerFailureCopyWithImpl<$Res, CustomerFailure>;
}
/// @nodoc
class _$CustomerFailureCopyWithImpl<$Res, $Val extends CustomerFailure>
implements $CustomerFailureCopyWith<$Res> {
_$CustomerFailureCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of CustomerFailure
/// with the given fields replaced by the non-null parameter values.
}
/// @nodoc
abstract class _$$ServerErrorImplCopyWith<$Res> {
factory _$$ServerErrorImplCopyWith(
_$ServerErrorImpl value,
$Res Function(_$ServerErrorImpl) then,
) = __$$ServerErrorImplCopyWithImpl<$Res>;
@useResult
$Res call({ApiFailure failure});
$ApiFailureCopyWith<$Res> get failure;
}
/// @nodoc
class __$$ServerErrorImplCopyWithImpl<$Res>
extends _$CustomerFailureCopyWithImpl<$Res, _$ServerErrorImpl>
implements _$$ServerErrorImplCopyWith<$Res> {
__$$ServerErrorImplCopyWithImpl(
_$ServerErrorImpl _value,
$Res Function(_$ServerErrorImpl) _then,
) : super(_value, _then);
/// Create a copy of CustomerFailure
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({Object? failure = null}) {
return _then(
_$ServerErrorImpl(
null == failure
? _value.failure
: failure // ignore: cast_nullable_to_non_nullable
as ApiFailure,
),
);
}
/// Create a copy of CustomerFailure
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$ApiFailureCopyWith<$Res> get failure {
return $ApiFailureCopyWith<$Res>(_value.failure, (value) {
return _then(_value.copyWith(failure: value));
});
}
}
/// @nodoc
class _$ServerErrorImpl implements _ServerError {
const _$ServerErrorImpl(this.failure);
@override
final ApiFailure failure;
@override
String toString() {
return 'CustomerFailure.serverError(failure: $failure)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$ServerErrorImpl &&
(identical(other.failure, failure) || other.failure == failure));
}
@override
int get hashCode => Object.hash(runtimeType, failure);
/// Create a copy of CustomerFailure
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$ServerErrorImplCopyWith<_$ServerErrorImpl> get copyWith =>
__$$ServerErrorImplCopyWithImpl<_$ServerErrorImpl>(this, _$identity);
@override
@optionalTypeArgs
TResult when<TResult extends Object?>({
required TResult Function(ApiFailure failure) serverError,
required TResult Function() unexpectedError,
required TResult Function(String erroMessage) dynamicErrorMessage,
}) {
return serverError(failure);
}
@override
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult? Function(ApiFailure failure)? serverError,
TResult? Function()? unexpectedError,
TResult? Function(String erroMessage)? dynamicErrorMessage,
}) {
return serverError?.call(failure);
}
@override
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
TResult Function(ApiFailure failure)? serverError,
TResult Function()? unexpectedError,
TResult Function(String erroMessage)? dynamicErrorMessage,
required TResult orElse(),
}) {
if (serverError != null) {
return serverError(failure);
}
return orElse();
}
@override
@optionalTypeArgs
TResult map<TResult extends Object?>({
required TResult Function(_ServerError value) serverError,
required TResult Function(_UnexpectedError value) unexpectedError,
required TResult Function(_DynamicErrorMessage value) dynamicErrorMessage,
}) {
return serverError(this);
}
@override
@optionalTypeArgs
TResult? mapOrNull<TResult extends Object?>({
TResult? Function(_ServerError value)? serverError,
TResult? Function(_UnexpectedError value)? unexpectedError,
TResult? Function(_DynamicErrorMessage value)? dynamicErrorMessage,
}) {
return serverError?.call(this);
}
@override
@optionalTypeArgs
TResult maybeMap<TResult extends Object?>({
TResult Function(_ServerError value)? serverError,
TResult Function(_UnexpectedError value)? unexpectedError,
TResult Function(_DynamicErrorMessage value)? dynamicErrorMessage,
required TResult orElse(),
}) {
if (serverError != null) {
return serverError(this);
}
return orElse();
}
}
abstract class _ServerError implements CustomerFailure {
const factory _ServerError(final ApiFailure failure) = _$ServerErrorImpl;
ApiFailure get failure;
/// Create a copy of CustomerFailure
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
_$$ServerErrorImplCopyWith<_$ServerErrorImpl> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class _$$UnexpectedErrorImplCopyWith<$Res> {
factory _$$UnexpectedErrorImplCopyWith(
_$UnexpectedErrorImpl value,
$Res Function(_$UnexpectedErrorImpl) then,
) = __$$UnexpectedErrorImplCopyWithImpl<$Res>;
}
/// @nodoc
class __$$UnexpectedErrorImplCopyWithImpl<$Res>
extends _$CustomerFailureCopyWithImpl<$Res, _$UnexpectedErrorImpl>
implements _$$UnexpectedErrorImplCopyWith<$Res> {
__$$UnexpectedErrorImplCopyWithImpl(
_$UnexpectedErrorImpl _value,
$Res Function(_$UnexpectedErrorImpl) _then,
) : super(_value, _then);
/// Create a copy of CustomerFailure
/// with the given fields replaced by the non-null parameter values.
}
/// @nodoc
class _$UnexpectedErrorImpl implements _UnexpectedError {
const _$UnexpectedErrorImpl();
@override
String toString() {
return 'CustomerFailure.unexpectedError()';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType && other is _$UnexpectedErrorImpl);
}
@override
int get hashCode => runtimeType.hashCode;
@override
@optionalTypeArgs
TResult when<TResult extends Object?>({
required TResult Function(ApiFailure failure) serverError,
required TResult Function() unexpectedError,
required TResult Function(String erroMessage) dynamicErrorMessage,
}) {
return unexpectedError();
}
@override
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult? Function(ApiFailure failure)? serverError,
TResult? Function()? unexpectedError,
TResult? Function(String erroMessage)? dynamicErrorMessage,
}) {
return unexpectedError?.call();
}
@override
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
TResult Function(ApiFailure failure)? serverError,
TResult Function()? unexpectedError,
TResult Function(String erroMessage)? dynamicErrorMessage,
required TResult orElse(),
}) {
if (unexpectedError != null) {
return unexpectedError();
}
return orElse();
}
@override
@optionalTypeArgs
TResult map<TResult extends Object?>({
required TResult Function(_ServerError value) serverError,
required TResult Function(_UnexpectedError value) unexpectedError,
required TResult Function(_DynamicErrorMessage value) dynamicErrorMessage,
}) {
return unexpectedError(this);
}
@override
@optionalTypeArgs
TResult? mapOrNull<TResult extends Object?>({
TResult? Function(_ServerError value)? serverError,
TResult? Function(_UnexpectedError value)? unexpectedError,
TResult? Function(_DynamicErrorMessage value)? dynamicErrorMessage,
}) {
return unexpectedError?.call(this);
}
@override
@optionalTypeArgs
TResult maybeMap<TResult extends Object?>({
TResult Function(_ServerError value)? serverError,
TResult Function(_UnexpectedError value)? unexpectedError,
TResult Function(_DynamicErrorMessage value)? dynamicErrorMessage,
required TResult orElse(),
}) {
if (unexpectedError != null) {
return unexpectedError(this);
}
return orElse();
}
}
abstract class _UnexpectedError implements CustomerFailure {
const factory _UnexpectedError() = _$UnexpectedErrorImpl;
}
/// @nodoc
abstract class _$$DynamicErrorMessageImplCopyWith<$Res> {
factory _$$DynamicErrorMessageImplCopyWith(
_$DynamicErrorMessageImpl value,
$Res Function(_$DynamicErrorMessageImpl) then,
) = __$$DynamicErrorMessageImplCopyWithImpl<$Res>;
@useResult
$Res call({String erroMessage});
}
/// @nodoc
class __$$DynamicErrorMessageImplCopyWithImpl<$Res>
extends _$CustomerFailureCopyWithImpl<$Res, _$DynamicErrorMessageImpl>
implements _$$DynamicErrorMessageImplCopyWith<$Res> {
__$$DynamicErrorMessageImplCopyWithImpl(
_$DynamicErrorMessageImpl _value,
$Res Function(_$DynamicErrorMessageImpl) _then,
) : super(_value, _then);
/// Create a copy of CustomerFailure
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({Object? erroMessage = null}) {
return _then(
_$DynamicErrorMessageImpl(
null == erroMessage
? _value.erroMessage
: erroMessage // ignore: cast_nullable_to_non_nullable
as String,
),
);
}
}
/// @nodoc
class _$DynamicErrorMessageImpl implements _DynamicErrorMessage {
const _$DynamicErrorMessageImpl(this.erroMessage);
@override
final String erroMessage;
@override
String toString() {
return 'CustomerFailure.dynamicErrorMessage(erroMessage: $erroMessage)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$DynamicErrorMessageImpl &&
(identical(other.erroMessage, erroMessage) ||
other.erroMessage == erroMessage));
}
@override
int get hashCode => Object.hash(runtimeType, erroMessage);
/// Create a copy of CustomerFailure
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$DynamicErrorMessageImplCopyWith<_$DynamicErrorMessageImpl> get copyWith =>
__$$DynamicErrorMessageImplCopyWithImpl<_$DynamicErrorMessageImpl>(
this,
_$identity,
);
@override
@optionalTypeArgs
TResult when<TResult extends Object?>({
required TResult Function(ApiFailure failure) serverError,
required TResult Function() unexpectedError,
required TResult Function(String erroMessage) dynamicErrorMessage,
}) {
return dynamicErrorMessage(erroMessage);
}
@override
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult? Function(ApiFailure failure)? serverError,
TResult? Function()? unexpectedError,
TResult? Function(String erroMessage)? dynamicErrorMessage,
}) {
return dynamicErrorMessage?.call(erroMessage);
}
@override
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
TResult Function(ApiFailure failure)? serverError,
TResult Function()? unexpectedError,
TResult Function(String erroMessage)? dynamicErrorMessage,
required TResult orElse(),
}) {
if (dynamicErrorMessage != null) {
return dynamicErrorMessage(erroMessage);
}
return orElse();
}
@override
@optionalTypeArgs
TResult map<TResult extends Object?>({
required TResult Function(_ServerError value) serverError,
required TResult Function(_UnexpectedError value) unexpectedError,
required TResult Function(_DynamicErrorMessage value) dynamicErrorMessage,
}) {
return dynamicErrorMessage(this);
}
@override
@optionalTypeArgs
TResult? mapOrNull<TResult extends Object?>({
TResult? Function(_ServerError value)? serverError,
TResult? Function(_UnexpectedError value)? unexpectedError,
TResult? Function(_DynamicErrorMessage value)? dynamicErrorMessage,
}) {
return dynamicErrorMessage?.call(this);
}
@override
@optionalTypeArgs
TResult maybeMap<TResult extends Object?>({
TResult Function(_ServerError value)? serverError,
TResult Function(_UnexpectedError value)? unexpectedError,
TResult Function(_DynamicErrorMessage value)? dynamicErrorMessage,
required TResult orElse(),
}) {
if (dynamicErrorMessage != null) {
return dynamicErrorMessage(this);
}
return orElse();
}
}
abstract class _DynamicErrorMessage implements CustomerFailure {
const factory _DynamicErrorMessage(final String erroMessage) =
_$DynamicErrorMessageImpl;
String get erroMessage;
/// Create a copy of CustomerFailure
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
_$$DynamicErrorMessageImplCopyWith<_$DynamicErrorMessageImpl> get copyWith =>
throw _privateConstructorUsedError;
}
@@ -0,0 +1,18 @@
part of '../customer.dart';
@freezed
class CustomerPoint with _$CustomerPoint {
const factory CustomerPoint({
required String status,
required String message,
required int totalPoints,
required String lastUpdated,
}) = _CustomerPoint;
factory CustomerPoint.empty() => const CustomerPoint(
status: '',
message: '',
totalPoints: 0,
lastUpdated: '',
);
}
@@ -0,0 +1,9 @@
part of '../customer.dart';
@freezed
sealed class CustomerFailure with _$CustomerFailure {
const factory CustomerFailure.serverError(ApiFailure failure) = _ServerError;
const factory CustomerFailure.unexpectedError() = _UnexpectedError;
const factory CustomerFailure.dynamicErrorMessage(String erroMessage) =
_DynamicErrorMessage;
}
@@ -0,0 +1,5 @@
part of '../customer.dart';
abstract class ICustomerRepository {
Future<Either<CustomerFailure, CustomerPoint>> getPoints();
}
+26
View File
@@ -0,0 +1,26 @@
part of '../game.dart';
@freezed
class Game with _$Game {
const factory Game({
required String id,
required String name,
required String type,
required bool isActive,
required Map<String, dynamic> metadata,
required List<GamePrize> prizes,
required String createdAt,
required String updatedAt,
}) = _Game;
factory Game.empty() => const Game(
id: '',
name: '',
type: '',
isActive: false,
metadata: {},
prizes: [],
createdAt: '',
updatedAt: '',
);
}
@@ -0,0 +1,22 @@
part of '../game.dart';
@freezed
class GamePrize with _$GamePrize {
const factory GamePrize({
required String id,
required String gameId,
required String name,
required Map<String, dynamic> metadata,
required String createdAt,
required String updatedAt,
}) = _GamePrize;
factory GamePrize.empty() => const GamePrize(
id: '',
gameId: '',
name: '',
metadata: {},
createdAt: '',
updatedAt: '',
);
}
@@ -0,0 +1,9 @@
part of '../game.dart';
@freezed
sealed class GameFailure with _$GameFailure {
const factory GameFailure.serverError(ApiFailure failure) = _ServerError;
const factory GameFailure.unexpectedError() = _UnexpectedError;
const factory GameFailure.dynamicErrorMessage(String erroMessage) =
_DynamicErrorMessage;
}
+11
View File
@@ -0,0 +1,11 @@
import 'package:dartz/dartz.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import '../../common/api/api_failure.dart';
part 'game.freezed.dart';
part 'entity/game_entity.dart';
part 'entity/game_prize_entity.dart';
part 'failures/game_failure.dart';
part 'repositories/i_game_repository.dart';
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,5 @@
part of '../game.dart';
abstract class IGameRepository {
Future<Either<GameFailure, Game>> ferrisWheel();
}
+3 -2
View File
@@ -9,12 +9,13 @@ abstract class Env {
@dev
class DevEnv implements Env {
@override
String get baseUrl => 'http://192.168.1.30:4000'; // example value
// String get baseUrl => 'http://192.168.1.30:4000'; // example value
String get baseUrl => 'https://api-pos.apskel.id'; // example value
}
@Injectable(as: Env)
@prod
class ProdEnv implements Env {
@override
String get baseUrl => 'https://enaklo-pos-be.altru.id';
String get baseUrl => 'https://api-pos.apskel.id';
}
@@ -211,7 +211,9 @@ class AuthRemoteDataProvider {
if ((response.data['errors'] as List).isNotEmpty) {
if (response.data['errors'][0]['code'] == "900") {
return DC.error(
AuthFailure.dynamicErrorMessage('Kamu Belum Terdaftar'),
AuthFailure.dynamicErrorMessage(
response.data['errors'][0]['cause'],
),
);
} else {
return DC.error(
@@ -0,0 +1,8 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import '../../domain/customer/customer.dart';
part 'customer_dtos.freezed.dart';
part 'customer_dtos.g.dart';
part 'dtos/customer_point_dto.dart';
@@ -0,0 +1,443 @@
// 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 'customer_dtos.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(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',
);
CustomerPointDto _$CustomerPointDtoFromJson(Map<String, dynamic> json) {
return _CustomerPointDto.fromJson(json);
}
/// @nodoc
mixin _$CustomerPointDto {
@JsonKey(name: 'status')
String? get status => throw _privateConstructorUsedError;
@JsonKey(name: 'message')
String? get message => throw _privateConstructorUsedError;
@JsonKey(name: 'data')
CustomerPointDataDto? get data => throw _privateConstructorUsedError;
/// Serializes this CustomerPointDto to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
/// Create a copy of CustomerPointDto
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$CustomerPointDtoCopyWith<CustomerPointDto> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $CustomerPointDtoCopyWith<$Res> {
factory $CustomerPointDtoCopyWith(
CustomerPointDto value,
$Res Function(CustomerPointDto) then,
) = _$CustomerPointDtoCopyWithImpl<$Res, CustomerPointDto>;
@useResult
$Res call({
@JsonKey(name: 'status') String? status,
@JsonKey(name: 'message') String? message,
@JsonKey(name: 'data') CustomerPointDataDto? data,
});
$CustomerPointDataDtoCopyWith<$Res>? get data;
}
/// @nodoc
class _$CustomerPointDtoCopyWithImpl<$Res, $Val extends CustomerPointDto>
implements $CustomerPointDtoCopyWith<$Res> {
_$CustomerPointDtoCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of CustomerPointDto
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? status = freezed,
Object? message = freezed,
Object? data = freezed,
}) {
return _then(
_value.copyWith(
status: freezed == status
? _value.status
: status // ignore: cast_nullable_to_non_nullable
as String?,
message: freezed == message
? _value.message
: message // ignore: cast_nullable_to_non_nullable
as String?,
data: freezed == data
? _value.data
: data // ignore: cast_nullable_to_non_nullable
as CustomerPointDataDto?,
)
as $Val,
);
}
/// Create a copy of CustomerPointDto
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$CustomerPointDataDtoCopyWith<$Res>? get data {
if (_value.data == null) {
return null;
}
return $CustomerPointDataDtoCopyWith<$Res>(_value.data!, (value) {
return _then(_value.copyWith(data: value) as $Val);
});
}
}
/// @nodoc
abstract class _$$CustomerPointDtoImplCopyWith<$Res>
implements $CustomerPointDtoCopyWith<$Res> {
factory _$$CustomerPointDtoImplCopyWith(
_$CustomerPointDtoImpl value,
$Res Function(_$CustomerPointDtoImpl) then,
) = __$$CustomerPointDtoImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({
@JsonKey(name: 'status') String? status,
@JsonKey(name: 'message') String? message,
@JsonKey(name: 'data') CustomerPointDataDto? data,
});
@override
$CustomerPointDataDtoCopyWith<$Res>? get data;
}
/// @nodoc
class __$$CustomerPointDtoImplCopyWithImpl<$Res>
extends _$CustomerPointDtoCopyWithImpl<$Res, _$CustomerPointDtoImpl>
implements _$$CustomerPointDtoImplCopyWith<$Res> {
__$$CustomerPointDtoImplCopyWithImpl(
_$CustomerPointDtoImpl _value,
$Res Function(_$CustomerPointDtoImpl) _then,
) : super(_value, _then);
/// Create a copy of CustomerPointDto
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? status = freezed,
Object? message = freezed,
Object? data = freezed,
}) {
return _then(
_$CustomerPointDtoImpl(
status: freezed == status
? _value.status
: status // ignore: cast_nullable_to_non_nullable
as String?,
message: freezed == message
? _value.message
: message // ignore: cast_nullable_to_non_nullable
as String?,
data: freezed == data
? _value.data
: data // ignore: cast_nullable_to_non_nullable
as CustomerPointDataDto?,
),
);
}
}
/// @nodoc
@JsonSerializable()
class _$CustomerPointDtoImpl extends _CustomerPointDto {
const _$CustomerPointDtoImpl({
@JsonKey(name: 'status') this.status,
@JsonKey(name: 'message') this.message,
@JsonKey(name: 'data') this.data,
}) : super._();
factory _$CustomerPointDtoImpl.fromJson(Map<String, dynamic> json) =>
_$$CustomerPointDtoImplFromJson(json);
@override
@JsonKey(name: 'status')
final String? status;
@override
@JsonKey(name: 'message')
final String? message;
@override
@JsonKey(name: 'data')
final CustomerPointDataDto? data;
@override
String toString() {
return 'CustomerPointDto(status: $status, message: $message, data: $data)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$CustomerPointDtoImpl &&
(identical(other.status, status) || other.status == status) &&
(identical(other.message, message) || other.message == message) &&
(identical(other.data, data) || other.data == data));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType, status, message, data);
/// Create a copy of CustomerPointDto
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$CustomerPointDtoImplCopyWith<_$CustomerPointDtoImpl> get copyWith =>
__$$CustomerPointDtoImplCopyWithImpl<_$CustomerPointDtoImpl>(
this,
_$identity,
);
@override
Map<String, dynamic> toJson() {
return _$$CustomerPointDtoImplToJson(this);
}
}
abstract class _CustomerPointDto extends CustomerPointDto {
const factory _CustomerPointDto({
@JsonKey(name: 'status') final String? status,
@JsonKey(name: 'message') final String? message,
@JsonKey(name: 'data') final CustomerPointDataDto? data,
}) = _$CustomerPointDtoImpl;
const _CustomerPointDto._() : super._();
factory _CustomerPointDto.fromJson(Map<String, dynamic> json) =
_$CustomerPointDtoImpl.fromJson;
@override
@JsonKey(name: 'status')
String? get status;
@override
@JsonKey(name: 'message')
String? get message;
@override
@JsonKey(name: 'data')
CustomerPointDataDto? get data;
/// Create a copy of CustomerPointDto
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
_$$CustomerPointDtoImplCopyWith<_$CustomerPointDtoImpl> get copyWith =>
throw _privateConstructorUsedError;
}
CustomerPointDataDto _$CustomerPointDataDtoFromJson(Map<String, dynamic> json) {
return _CustomerPointDataDto.fromJson(json);
}
/// @nodoc
mixin _$CustomerPointDataDto {
@JsonKey(name: 'total_points')
int? get totalPoints => throw _privateConstructorUsedError;
@JsonKey(name: 'last_updated')
String? get lastUpdated => throw _privateConstructorUsedError;
/// Serializes this CustomerPointDataDto to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
/// Create a copy of CustomerPointDataDto
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$CustomerPointDataDtoCopyWith<CustomerPointDataDto> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $CustomerPointDataDtoCopyWith<$Res> {
factory $CustomerPointDataDtoCopyWith(
CustomerPointDataDto value,
$Res Function(CustomerPointDataDto) then,
) = _$CustomerPointDataDtoCopyWithImpl<$Res, CustomerPointDataDto>;
@useResult
$Res call({
@JsonKey(name: 'total_points') int? totalPoints,
@JsonKey(name: 'last_updated') String? lastUpdated,
});
}
/// @nodoc
class _$CustomerPointDataDtoCopyWithImpl<
$Res,
$Val extends CustomerPointDataDto
>
implements $CustomerPointDataDtoCopyWith<$Res> {
_$CustomerPointDataDtoCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of CustomerPointDataDto
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({Object? totalPoints = freezed, Object? lastUpdated = freezed}) {
return _then(
_value.copyWith(
totalPoints: freezed == totalPoints
? _value.totalPoints
: totalPoints // ignore: cast_nullable_to_non_nullable
as int?,
lastUpdated: freezed == lastUpdated
? _value.lastUpdated
: lastUpdated // ignore: cast_nullable_to_non_nullable
as String?,
)
as $Val,
);
}
}
/// @nodoc
abstract class _$$CustomerPointDataDtoImplCopyWith<$Res>
implements $CustomerPointDataDtoCopyWith<$Res> {
factory _$$CustomerPointDataDtoImplCopyWith(
_$CustomerPointDataDtoImpl value,
$Res Function(_$CustomerPointDataDtoImpl) then,
) = __$$CustomerPointDataDtoImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({
@JsonKey(name: 'total_points') int? totalPoints,
@JsonKey(name: 'last_updated') String? lastUpdated,
});
}
/// @nodoc
class __$$CustomerPointDataDtoImplCopyWithImpl<$Res>
extends _$CustomerPointDataDtoCopyWithImpl<$Res, _$CustomerPointDataDtoImpl>
implements _$$CustomerPointDataDtoImplCopyWith<$Res> {
__$$CustomerPointDataDtoImplCopyWithImpl(
_$CustomerPointDataDtoImpl _value,
$Res Function(_$CustomerPointDataDtoImpl) _then,
) : super(_value, _then);
/// Create a copy of CustomerPointDataDto
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({Object? totalPoints = freezed, Object? lastUpdated = freezed}) {
return _then(
_$CustomerPointDataDtoImpl(
totalPoints: freezed == totalPoints
? _value.totalPoints
: totalPoints // ignore: cast_nullable_to_non_nullable
as int?,
lastUpdated: freezed == lastUpdated
? _value.lastUpdated
: lastUpdated // ignore: cast_nullable_to_non_nullable
as String?,
),
);
}
}
/// @nodoc
@JsonSerializable()
class _$CustomerPointDataDtoImpl implements _CustomerPointDataDto {
const _$CustomerPointDataDtoImpl({
@JsonKey(name: 'total_points') this.totalPoints,
@JsonKey(name: 'last_updated') this.lastUpdated,
});
factory _$CustomerPointDataDtoImpl.fromJson(Map<String, dynamic> json) =>
_$$CustomerPointDataDtoImplFromJson(json);
@override
@JsonKey(name: 'total_points')
final int? totalPoints;
@override
@JsonKey(name: 'last_updated')
final String? lastUpdated;
@override
String toString() {
return 'CustomerPointDataDto(totalPoints: $totalPoints, lastUpdated: $lastUpdated)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$CustomerPointDataDtoImpl &&
(identical(other.totalPoints, totalPoints) ||
other.totalPoints == totalPoints) &&
(identical(other.lastUpdated, lastUpdated) ||
other.lastUpdated == lastUpdated));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType, totalPoints, lastUpdated);
/// Create a copy of CustomerPointDataDto
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$CustomerPointDataDtoImplCopyWith<_$CustomerPointDataDtoImpl>
get copyWith =>
__$$CustomerPointDataDtoImplCopyWithImpl<_$CustomerPointDataDtoImpl>(
this,
_$identity,
);
@override
Map<String, dynamic> toJson() {
return _$$CustomerPointDataDtoImplToJson(this);
}
}
abstract class _CustomerPointDataDto implements CustomerPointDataDto {
const factory _CustomerPointDataDto({
@JsonKey(name: 'total_points') final int? totalPoints,
@JsonKey(name: 'last_updated') final String? lastUpdated,
}) = _$CustomerPointDataDtoImpl;
factory _CustomerPointDataDto.fromJson(Map<String, dynamic> json) =
_$CustomerPointDataDtoImpl.fromJson;
@override
@JsonKey(name: 'total_points')
int? get totalPoints;
@override
@JsonKey(name: 'last_updated')
String? get lastUpdated;
/// Create a copy of CustomerPointDataDto
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
_$$CustomerPointDataDtoImplCopyWith<_$CustomerPointDataDtoImpl>
get copyWith => throw _privateConstructorUsedError;
}
@@ -0,0 +1,39 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'customer_dtos.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_$CustomerPointDtoImpl _$$CustomerPointDtoImplFromJson(
Map<String, dynamic> json,
) => _$CustomerPointDtoImpl(
status: json['status'] as String?,
message: json['message'] as String?,
data: json['data'] == null
? null
: CustomerPointDataDto.fromJson(json['data'] as Map<String, dynamic>),
);
Map<String, dynamic> _$$CustomerPointDtoImplToJson(
_$CustomerPointDtoImpl instance,
) => <String, dynamic>{
'status': instance.status,
'message': instance.message,
'data': instance.data,
};
_$CustomerPointDataDtoImpl _$$CustomerPointDataDtoImplFromJson(
Map<String, dynamic> json,
) => _$CustomerPointDataDtoImpl(
totalPoints: (json['total_points'] as num?)?.toInt(),
lastUpdated: json['last_updated'] as String?,
);
Map<String, dynamic> _$$CustomerPointDataDtoImplToJson(
_$CustomerPointDataDtoImpl instance,
) => <String, dynamic>{
'total_points': instance.totalPoints,
'last_updated': instance.lastUpdated,
};
@@ -0,0 +1,50 @@
import 'dart:developer';
import 'package:data_channel/data_channel.dart';
import 'package:injectable/injectable.dart';
import '../../../common/api/api_client.dart';
import '../../../common/api/api_failure.dart';
import '../../../common/function/app_function.dart';
import '../../../common/url/api_path.dart';
import '../../../domain/customer/customer.dart';
import '../customer_dtos.dart';
@injectable
class CustomerRemoteDataProvider {
final ApiClient _apiClient;
final String _logName = "CustomerRemoteDataProvider";
CustomerRemoteDataProvider(this._apiClient);
Future<DC<CustomerFailure, CustomerPointDto>> fetchCustomerPoint() async {
try {
final response = await _apiClient.get(
ApiPath.customerPoint,
headers: getAuthorizationHeader(),
);
if (response.data['code'] == 401) {
return DC.error(
CustomerFailure.serverError(
ApiFailure.unauthorized('Session Expired'),
),
);
}
if (response.data['status'] == false) {
return DC.error(
CustomerFailure.dynamicErrorMessage(
'Terjadi kesalahan coba lagi nanti',
),
);
}
final dto = CustomerPointDto.fromJson(response.data['data']);
return DC.data(dto);
} on ApiFailure catch (e, s) {
log('fetchCustomerPoint', name: _logName, error: e, stackTrace: s);
return DC.error(CustomerFailure.serverError(e));
}
}
}
@@ -0,0 +1,34 @@
part of '../customer_dtos.dart';
@freezed
class CustomerPointDto with _$CustomerPointDto {
const factory CustomerPointDto({
@JsonKey(name: 'status') String? status,
@JsonKey(name: 'message') String? message,
@JsonKey(name: 'data') CustomerPointDataDto? data,
}) = _CustomerPointDto;
factory CustomerPointDto.fromJson(Map<String, dynamic> json) =>
_$CustomerPointDtoFromJson(json);
const CustomerPointDto._();
/// mapping ke domain
CustomerPoint toDomain() => CustomerPoint(
status: status ?? '',
message: message ?? '',
totalPoints: data?.totalPoints ?? 0,
lastUpdated: data?.lastUpdated ?? '',
);
}
@freezed
class CustomerPointDataDto with _$CustomerPointDataDto {
const factory CustomerPointDataDto({
@JsonKey(name: 'total_points') int? totalPoints,
@JsonKey(name: 'last_updated') String? lastUpdated,
}) = _CustomerPointDataDto;
factory CustomerPointDataDto.fromJson(Map<String, dynamic> json) =>
_$CustomerPointDataDtoFromJson(json);
}
@@ -0,0 +1,33 @@
import 'dart:developer';
import 'package:dartz/dartz.dart';
import 'package:injectable/injectable.dart';
import '../../../domain/customer/customer.dart';
import '../datasources/remote_data_provider.dart';
@Injectable(as: ICustomerRepository)
class CustomerRepository implements ICustomerRepository {
final CustomerRemoteDataProvider _remoteDataProvider;
final String _logName = 'CustomerRepository';
CustomerRepository(this._remoteDataProvider);
@override
Future<Either<CustomerFailure, CustomerPoint>> getPoints() async {
try {
final result = await _remoteDataProvider.fetchCustomerPoint();
if (result.hasError) {
return left(result.error!);
}
final data = result.data!.toDomain();
return right(data);
} catch (e, s) {
log('getPoints', name: _logName, error: e, stackTrace: s);
return left(const CustomerFailure.unexpectedError());
}
}
}
@@ -0,0 +1,47 @@
import 'dart:developer';
import 'package:data_channel/data_channel.dart';
import 'package:injectable/injectable.dart';
import '../../../common/api/api_client.dart';
import '../../../common/api/api_failure.dart';
import '../../../common/function/app_function.dart';
import '../../../common/url/api_path.dart';
import '../../../domain/game/game.dart';
import '../game_dtos.dart';
@injectable
class GameRemoteDataProvider {
final ApiClient _apiClient;
final String _logName = "GameRemoteDataProvider";
GameRemoteDataProvider(this._apiClient);
Future<DC<GameFailure, GameDto>> ferrisWheel() async {
try {
final response = await _apiClient.get(
ApiPath.ferrisWheel,
headers: getAuthorizationHeader(),
);
if (response.data['code'] == 401) {
return DC.error(
GameFailure.serverError(ApiFailure.unauthorized('Session Expired')),
);
}
if (response.data['status'] == false) {
return DC.error(
GameFailure.dynamicErrorMessage('Terjadi kesalahan coba lagi nanti'),
);
}
final dto = GameDto.fromJson(response.data['data']['data']['game']);
return DC.data(dto);
} on ApiFailure catch (e, s) {
log('ferrisWheel', name: _logName, error: e, stackTrace: s);
return DC.error(GameFailure.serverError(e));
}
}
}
+31
View File
@@ -0,0 +1,31 @@
part of '../game_dtos.dart';
@freezed
class GameDto with _$GameDto {
const factory GameDto({
@JsonKey(name: 'id') String? id,
@JsonKey(name: 'name') String? name,
@JsonKey(name: 'type') String? type,
@JsonKey(name: 'is_active') bool? isActive,
@JsonKey(name: 'metadata') Map<String, dynamic>? metadata,
@JsonKey(name: 'prizes') List<GamePrizeDto>? prizes,
@JsonKey(name: 'created_at') String? createdAt,
@JsonKey(name: 'updated_at') String? updatedAt,
}) = _GameDto;
factory GameDto.fromJson(Map<String, dynamic> json) =>
_$GameDtoFromJson(json);
const GameDto._();
Game toDomain() => Game(
id: id ?? '',
name: name ?? '',
type: type ?? '',
isActive: isActive ?? false,
metadata: metadata ?? {},
prizes: prizes?.map((e) => e.toDomain()).toList() ?? [],
createdAt: createdAt ?? '',
updatedAt: updatedAt ?? '',
);
}
@@ -0,0 +1,27 @@
part of '../game_dtos.dart';
@freezed
class GamePrizeDto with _$GamePrizeDto {
const factory GamePrizeDto({
@JsonKey(name: 'id') String? id,
@JsonKey(name: 'game_id') String? gameId,
@JsonKey(name: 'name') String? name,
@JsonKey(name: 'metadata') Map<String, dynamic>? metadata,
@JsonKey(name: 'created_at') String? createdAt,
@JsonKey(name: 'updated_at') String? updatedAt,
}) = _GamePrizeDto;
factory GamePrizeDto.fromJson(Map<String, dynamic> json) =>
_$GamePrizeDtoFromJson(json);
const GamePrizeDto._();
GamePrize toDomain() => GamePrize(
id: id ?? '',
gameId: gameId ?? '',
name: name ?? '',
metadata: metadata ?? {},
createdAt: createdAt ?? '',
updatedAt: updatedAt ?? '',
);
}
+9
View File
@@ -0,0 +1,9 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import '../../domain/game/game.dart';
part 'game_dtos.freezed.dart';
part 'game_dtos.g.dart';
part 'dto/game_dto.dart';
part 'dto/game_prize_dto.dart';
@@ -0,0 +1,671 @@
// 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 'game_dtos.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(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',
);
GameDto _$GameDtoFromJson(Map<String, dynamic> json) {
return _GameDto.fromJson(json);
}
/// @nodoc
mixin _$GameDto {
@JsonKey(name: 'id')
String? get id => throw _privateConstructorUsedError;
@JsonKey(name: 'name')
String? get name => throw _privateConstructorUsedError;
@JsonKey(name: 'type')
String? get type => throw _privateConstructorUsedError;
@JsonKey(name: 'is_active')
bool? get isActive => throw _privateConstructorUsedError;
@JsonKey(name: 'metadata')
Map<String, dynamic>? get metadata => throw _privateConstructorUsedError;
@JsonKey(name: 'prizes')
List<GamePrizeDto>? get prizes => throw _privateConstructorUsedError;
@JsonKey(name: 'created_at')
String? get createdAt => throw _privateConstructorUsedError;
@JsonKey(name: 'updated_at')
String? get updatedAt => throw _privateConstructorUsedError;
/// Serializes this GameDto to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
/// Create a copy of GameDto
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$GameDtoCopyWith<GameDto> get copyWith => throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $GameDtoCopyWith<$Res> {
factory $GameDtoCopyWith(GameDto value, $Res Function(GameDto) then) =
_$GameDtoCopyWithImpl<$Res, GameDto>;
@useResult
$Res call({
@JsonKey(name: 'id') String? id,
@JsonKey(name: 'name') String? name,
@JsonKey(name: 'type') String? type,
@JsonKey(name: 'is_active') bool? isActive,
@JsonKey(name: 'metadata') Map<String, dynamic>? metadata,
@JsonKey(name: 'prizes') List<GamePrizeDto>? prizes,
@JsonKey(name: 'created_at') String? createdAt,
@JsonKey(name: 'updated_at') String? updatedAt,
});
}
/// @nodoc
class _$GameDtoCopyWithImpl<$Res, $Val extends GameDto>
implements $GameDtoCopyWith<$Res> {
_$GameDtoCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of GameDto
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? id = freezed,
Object? name = freezed,
Object? type = freezed,
Object? isActive = freezed,
Object? metadata = freezed,
Object? prizes = freezed,
Object? createdAt = freezed,
Object? updatedAt = freezed,
}) {
return _then(
_value.copyWith(
id: freezed == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as String?,
name: freezed == name
? _value.name
: name // ignore: cast_nullable_to_non_nullable
as String?,
type: freezed == type
? _value.type
: type // ignore: cast_nullable_to_non_nullable
as String?,
isActive: freezed == isActive
? _value.isActive
: isActive // ignore: cast_nullable_to_non_nullable
as bool?,
metadata: freezed == metadata
? _value.metadata
: metadata // ignore: cast_nullable_to_non_nullable
as Map<String, dynamic>?,
prizes: freezed == prizes
? _value.prizes
: prizes // ignore: cast_nullable_to_non_nullable
as List<GamePrizeDto>?,
createdAt: freezed == createdAt
? _value.createdAt
: createdAt // ignore: cast_nullable_to_non_nullable
as String?,
updatedAt: freezed == updatedAt
? _value.updatedAt
: updatedAt // ignore: cast_nullable_to_non_nullable
as String?,
)
as $Val,
);
}
}
/// @nodoc
abstract class _$$GameDtoImplCopyWith<$Res> implements $GameDtoCopyWith<$Res> {
factory _$$GameDtoImplCopyWith(
_$GameDtoImpl value,
$Res Function(_$GameDtoImpl) then,
) = __$$GameDtoImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({
@JsonKey(name: 'id') String? id,
@JsonKey(name: 'name') String? name,
@JsonKey(name: 'type') String? type,
@JsonKey(name: 'is_active') bool? isActive,
@JsonKey(name: 'metadata') Map<String, dynamic>? metadata,
@JsonKey(name: 'prizes') List<GamePrizeDto>? prizes,
@JsonKey(name: 'created_at') String? createdAt,
@JsonKey(name: 'updated_at') String? updatedAt,
});
}
/// @nodoc
class __$$GameDtoImplCopyWithImpl<$Res>
extends _$GameDtoCopyWithImpl<$Res, _$GameDtoImpl>
implements _$$GameDtoImplCopyWith<$Res> {
__$$GameDtoImplCopyWithImpl(
_$GameDtoImpl _value,
$Res Function(_$GameDtoImpl) _then,
) : super(_value, _then);
/// Create a copy of GameDto
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? id = freezed,
Object? name = freezed,
Object? type = freezed,
Object? isActive = freezed,
Object? metadata = freezed,
Object? prizes = freezed,
Object? createdAt = freezed,
Object? updatedAt = freezed,
}) {
return _then(
_$GameDtoImpl(
id: freezed == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as String?,
name: freezed == name
? _value.name
: name // ignore: cast_nullable_to_non_nullable
as String?,
type: freezed == type
? _value.type
: type // ignore: cast_nullable_to_non_nullable
as String?,
isActive: freezed == isActive
? _value.isActive
: isActive // ignore: cast_nullable_to_non_nullable
as bool?,
metadata: freezed == metadata
? _value._metadata
: metadata // ignore: cast_nullable_to_non_nullable
as Map<String, dynamic>?,
prizes: freezed == prizes
? _value._prizes
: prizes // ignore: cast_nullable_to_non_nullable
as List<GamePrizeDto>?,
createdAt: freezed == createdAt
? _value.createdAt
: createdAt // ignore: cast_nullable_to_non_nullable
as String?,
updatedAt: freezed == updatedAt
? _value.updatedAt
: updatedAt // ignore: cast_nullable_to_non_nullable
as String?,
),
);
}
}
/// @nodoc
@JsonSerializable()
class _$GameDtoImpl extends _GameDto {
const _$GameDtoImpl({
@JsonKey(name: 'id') this.id,
@JsonKey(name: 'name') this.name,
@JsonKey(name: 'type') this.type,
@JsonKey(name: 'is_active') this.isActive,
@JsonKey(name: 'metadata') final Map<String, dynamic>? metadata,
@JsonKey(name: 'prizes') final List<GamePrizeDto>? prizes,
@JsonKey(name: 'created_at') this.createdAt,
@JsonKey(name: 'updated_at') this.updatedAt,
}) : _metadata = metadata,
_prizes = prizes,
super._();
factory _$GameDtoImpl.fromJson(Map<String, dynamic> json) =>
_$$GameDtoImplFromJson(json);
@override
@JsonKey(name: 'id')
final String? id;
@override
@JsonKey(name: 'name')
final String? name;
@override
@JsonKey(name: 'type')
final String? type;
@override
@JsonKey(name: 'is_active')
final bool? isActive;
final Map<String, dynamic>? _metadata;
@override
@JsonKey(name: 'metadata')
Map<String, dynamic>? get metadata {
final value = _metadata;
if (value == null) return null;
if (_metadata is EqualUnmodifiableMapView) return _metadata;
// ignore: implicit_dynamic_type
return EqualUnmodifiableMapView(value);
}
final List<GamePrizeDto>? _prizes;
@override
@JsonKey(name: 'prizes')
List<GamePrizeDto>? get prizes {
final value = _prizes;
if (value == null) return null;
if (_prizes is EqualUnmodifiableListView) return _prizes;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(value);
}
@override
@JsonKey(name: 'created_at')
final String? createdAt;
@override
@JsonKey(name: 'updated_at')
final String? updatedAt;
@override
String toString() {
return 'GameDto(id: $id, name: $name, type: $type, isActive: $isActive, metadata: $metadata, prizes: $prizes, createdAt: $createdAt, updatedAt: $updatedAt)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$GameDtoImpl &&
(identical(other.id, id) || other.id == id) &&
(identical(other.name, name) || other.name == name) &&
(identical(other.type, type) || other.type == type) &&
(identical(other.isActive, isActive) ||
other.isActive == isActive) &&
const DeepCollectionEquality().equals(other._metadata, _metadata) &&
const DeepCollectionEquality().equals(other._prizes, _prizes) &&
(identical(other.createdAt, createdAt) ||
other.createdAt == createdAt) &&
(identical(other.updatedAt, updatedAt) ||
other.updatedAt == updatedAt));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(
runtimeType,
id,
name,
type,
isActive,
const DeepCollectionEquality().hash(_metadata),
const DeepCollectionEquality().hash(_prizes),
createdAt,
updatedAt,
);
/// Create a copy of GameDto
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$GameDtoImplCopyWith<_$GameDtoImpl> get copyWith =>
__$$GameDtoImplCopyWithImpl<_$GameDtoImpl>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$$GameDtoImplToJson(this);
}
}
abstract class _GameDto extends GameDto {
const factory _GameDto({
@JsonKey(name: 'id') final String? id,
@JsonKey(name: 'name') final String? name,
@JsonKey(name: 'type') final String? type,
@JsonKey(name: 'is_active') final bool? isActive,
@JsonKey(name: 'metadata') final Map<String, dynamic>? metadata,
@JsonKey(name: 'prizes') final List<GamePrizeDto>? prizes,
@JsonKey(name: 'created_at') final String? createdAt,
@JsonKey(name: 'updated_at') final String? updatedAt,
}) = _$GameDtoImpl;
const _GameDto._() : super._();
factory _GameDto.fromJson(Map<String, dynamic> json) = _$GameDtoImpl.fromJson;
@override
@JsonKey(name: 'id')
String? get id;
@override
@JsonKey(name: 'name')
String? get name;
@override
@JsonKey(name: 'type')
String? get type;
@override
@JsonKey(name: 'is_active')
bool? get isActive;
@override
@JsonKey(name: 'metadata')
Map<String, dynamic>? get metadata;
@override
@JsonKey(name: 'prizes')
List<GamePrizeDto>? get prizes;
@override
@JsonKey(name: 'created_at')
String? get createdAt;
@override
@JsonKey(name: 'updated_at')
String? get updatedAt;
/// Create a copy of GameDto
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
_$$GameDtoImplCopyWith<_$GameDtoImpl> get copyWith =>
throw _privateConstructorUsedError;
}
GamePrizeDto _$GamePrizeDtoFromJson(Map<String, dynamic> json) {
return _GamePrizeDto.fromJson(json);
}
/// @nodoc
mixin _$GamePrizeDto {
@JsonKey(name: 'id')
String? get id => throw _privateConstructorUsedError;
@JsonKey(name: 'game_id')
String? get gameId => throw _privateConstructorUsedError;
@JsonKey(name: 'name')
String? get name => throw _privateConstructorUsedError;
@JsonKey(name: 'metadata')
Map<String, dynamic>? get metadata => throw _privateConstructorUsedError;
@JsonKey(name: 'created_at')
String? get createdAt => throw _privateConstructorUsedError;
@JsonKey(name: 'updated_at')
String? get updatedAt => throw _privateConstructorUsedError;
/// Serializes this GamePrizeDto to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
/// Create a copy of GamePrizeDto
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$GamePrizeDtoCopyWith<GamePrizeDto> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $GamePrizeDtoCopyWith<$Res> {
factory $GamePrizeDtoCopyWith(
GamePrizeDto value,
$Res Function(GamePrizeDto) then,
) = _$GamePrizeDtoCopyWithImpl<$Res, GamePrizeDto>;
@useResult
$Res call({
@JsonKey(name: 'id') String? id,
@JsonKey(name: 'game_id') String? gameId,
@JsonKey(name: 'name') String? name,
@JsonKey(name: 'metadata') Map<String, dynamic>? metadata,
@JsonKey(name: 'created_at') String? createdAt,
@JsonKey(name: 'updated_at') String? updatedAt,
});
}
/// @nodoc
class _$GamePrizeDtoCopyWithImpl<$Res, $Val extends GamePrizeDto>
implements $GamePrizeDtoCopyWith<$Res> {
_$GamePrizeDtoCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of GamePrizeDto
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? id = freezed,
Object? gameId = freezed,
Object? name = freezed,
Object? metadata = freezed,
Object? createdAt = freezed,
Object? updatedAt = freezed,
}) {
return _then(
_value.copyWith(
id: freezed == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as String?,
gameId: freezed == gameId
? _value.gameId
: gameId // ignore: cast_nullable_to_non_nullable
as String?,
name: freezed == name
? _value.name
: name // ignore: cast_nullable_to_non_nullable
as String?,
metadata: freezed == metadata
? _value.metadata
: metadata // ignore: cast_nullable_to_non_nullable
as Map<String, dynamic>?,
createdAt: freezed == createdAt
? _value.createdAt
: createdAt // ignore: cast_nullable_to_non_nullable
as String?,
updatedAt: freezed == updatedAt
? _value.updatedAt
: updatedAt // ignore: cast_nullable_to_non_nullable
as String?,
)
as $Val,
);
}
}
/// @nodoc
abstract class _$$GamePrizeDtoImplCopyWith<$Res>
implements $GamePrizeDtoCopyWith<$Res> {
factory _$$GamePrizeDtoImplCopyWith(
_$GamePrizeDtoImpl value,
$Res Function(_$GamePrizeDtoImpl) then,
) = __$$GamePrizeDtoImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({
@JsonKey(name: 'id') String? id,
@JsonKey(name: 'game_id') String? gameId,
@JsonKey(name: 'name') String? name,
@JsonKey(name: 'metadata') Map<String, dynamic>? metadata,
@JsonKey(name: 'created_at') String? createdAt,
@JsonKey(name: 'updated_at') String? updatedAt,
});
}
/// @nodoc
class __$$GamePrizeDtoImplCopyWithImpl<$Res>
extends _$GamePrizeDtoCopyWithImpl<$Res, _$GamePrizeDtoImpl>
implements _$$GamePrizeDtoImplCopyWith<$Res> {
__$$GamePrizeDtoImplCopyWithImpl(
_$GamePrizeDtoImpl _value,
$Res Function(_$GamePrizeDtoImpl) _then,
) : super(_value, _then);
/// Create a copy of GamePrizeDto
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? id = freezed,
Object? gameId = freezed,
Object? name = freezed,
Object? metadata = freezed,
Object? createdAt = freezed,
Object? updatedAt = freezed,
}) {
return _then(
_$GamePrizeDtoImpl(
id: freezed == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as String?,
gameId: freezed == gameId
? _value.gameId
: gameId // ignore: cast_nullable_to_non_nullable
as String?,
name: freezed == name
? _value.name
: name // ignore: cast_nullable_to_non_nullable
as String?,
metadata: freezed == metadata
? _value._metadata
: metadata // ignore: cast_nullable_to_non_nullable
as Map<String, dynamic>?,
createdAt: freezed == createdAt
? _value.createdAt
: createdAt // ignore: cast_nullable_to_non_nullable
as String?,
updatedAt: freezed == updatedAt
? _value.updatedAt
: updatedAt // ignore: cast_nullable_to_non_nullable
as String?,
),
);
}
}
/// @nodoc
@JsonSerializable()
class _$GamePrizeDtoImpl extends _GamePrizeDto {
const _$GamePrizeDtoImpl({
@JsonKey(name: 'id') this.id,
@JsonKey(name: 'game_id') this.gameId,
@JsonKey(name: 'name') this.name,
@JsonKey(name: 'metadata') final Map<String, dynamic>? metadata,
@JsonKey(name: 'created_at') this.createdAt,
@JsonKey(name: 'updated_at') this.updatedAt,
}) : _metadata = metadata,
super._();
factory _$GamePrizeDtoImpl.fromJson(Map<String, dynamic> json) =>
_$$GamePrizeDtoImplFromJson(json);
@override
@JsonKey(name: 'id')
final String? id;
@override
@JsonKey(name: 'game_id')
final String? gameId;
@override
@JsonKey(name: 'name')
final String? name;
final Map<String, dynamic>? _metadata;
@override
@JsonKey(name: 'metadata')
Map<String, dynamic>? get metadata {
final value = _metadata;
if (value == null) return null;
if (_metadata is EqualUnmodifiableMapView) return _metadata;
// ignore: implicit_dynamic_type
return EqualUnmodifiableMapView(value);
}
@override
@JsonKey(name: 'created_at')
final String? createdAt;
@override
@JsonKey(name: 'updated_at')
final String? updatedAt;
@override
String toString() {
return 'GamePrizeDto(id: $id, gameId: $gameId, name: $name, metadata: $metadata, createdAt: $createdAt, updatedAt: $updatedAt)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$GamePrizeDtoImpl &&
(identical(other.id, id) || other.id == id) &&
(identical(other.gameId, gameId) || other.gameId == gameId) &&
(identical(other.name, name) || other.name == name) &&
const DeepCollectionEquality().equals(other._metadata, _metadata) &&
(identical(other.createdAt, createdAt) ||
other.createdAt == createdAt) &&
(identical(other.updatedAt, updatedAt) ||
other.updatedAt == updatedAt));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(
runtimeType,
id,
gameId,
name,
const DeepCollectionEquality().hash(_metadata),
createdAt,
updatedAt,
);
/// Create a copy of GamePrizeDto
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$GamePrizeDtoImplCopyWith<_$GamePrizeDtoImpl> get copyWith =>
__$$GamePrizeDtoImplCopyWithImpl<_$GamePrizeDtoImpl>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$$GamePrizeDtoImplToJson(this);
}
}
abstract class _GamePrizeDto extends GamePrizeDto {
const factory _GamePrizeDto({
@JsonKey(name: 'id') final String? id,
@JsonKey(name: 'game_id') final String? gameId,
@JsonKey(name: 'name') final String? name,
@JsonKey(name: 'metadata') final Map<String, dynamic>? metadata,
@JsonKey(name: 'created_at') final String? createdAt,
@JsonKey(name: 'updated_at') final String? updatedAt,
}) = _$GamePrizeDtoImpl;
const _GamePrizeDto._() : super._();
factory _GamePrizeDto.fromJson(Map<String, dynamic> json) =
_$GamePrizeDtoImpl.fromJson;
@override
@JsonKey(name: 'id')
String? get id;
@override
@JsonKey(name: 'game_id')
String? get gameId;
@override
@JsonKey(name: 'name')
String? get name;
@override
@JsonKey(name: 'metadata')
Map<String, dynamic>? get metadata;
@override
@JsonKey(name: 'created_at')
String? get createdAt;
@override
@JsonKey(name: 'updated_at')
String? get updatedAt;
/// Create a copy of GamePrizeDto
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
_$$GamePrizeDtoImplCopyWith<_$GamePrizeDtoImpl> get copyWith =>
throw _privateConstructorUsedError;
}
+53
View File
@@ -0,0 +1,53 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'game_dtos.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_$GameDtoImpl _$$GameDtoImplFromJson(Map<String, dynamic> json) =>
_$GameDtoImpl(
id: json['id'] as String?,
name: json['name'] as String?,
type: json['type'] as String?,
isActive: json['is_active'] as bool?,
metadata: json['metadata'] as Map<String, dynamic>?,
prizes: (json['prizes'] as List<dynamic>?)
?.map((e) => GamePrizeDto.fromJson(e as Map<String, dynamic>))
.toList(),
createdAt: json['created_at'] as String?,
updatedAt: json['updated_at'] as String?,
);
Map<String, dynamic> _$$GameDtoImplToJson(_$GameDtoImpl instance) =>
<String, dynamic>{
'id': instance.id,
'name': instance.name,
'type': instance.type,
'is_active': instance.isActive,
'metadata': instance.metadata,
'prizes': instance.prizes,
'created_at': instance.createdAt,
'updated_at': instance.updatedAt,
};
_$GamePrizeDtoImpl _$$GamePrizeDtoImplFromJson(Map<String, dynamic> json) =>
_$GamePrizeDtoImpl(
id: json['id'] as String?,
gameId: json['game_id'] as String?,
name: json['name'] as String?,
metadata: json['metadata'] as Map<String, dynamic>?,
createdAt: json['created_at'] as String?,
updatedAt: json['updated_at'] as String?,
);
Map<String, dynamic> _$$GamePrizeDtoImplToJson(_$GamePrizeDtoImpl instance) =>
<String, dynamic>{
'id': instance.id,
'game_id': instance.gameId,
'name': instance.name,
'metadata': instance.metadata,
'created_at': instance.createdAt,
'updated_at': instance.updatedAt,
};
@@ -0,0 +1,34 @@
import 'dart:developer';
import 'package:dartz/dartz.dart';
import 'package:injectable/injectable.dart';
import '../../../domain/game/game.dart';
import '../datasources/remote_data_provider.dart';
@Injectable(as: IGameRepository)
class GameRepository implements IGameRepository {
final GameRemoteDataProvider _remoteDataProvider;
final String _logName = 'GameRepository';
GameRepository(this._remoteDataProvider);
@override
Future<Either<GameFailure, Game>> ferrisWheel() async {
try {
final result = await _remoteDataProvider.ferrisWheel();
if (result.hasError) {
return left(result.error!);
}
final data = result.data!.toDomain();
return right(data);
} catch (e, s) {
log('ferrisWheel', name: _logName, error: e, stackTrace: s);
return left(const GameFailure.unexpectedError());
}
}
}
+40 -3
View File
@@ -16,6 +16,8 @@ import 'package:enaklo/application/auth/check_phone_form/check_phone_form_bloc.d
as _i869;
import 'package:enaklo/application/auth/login_form/login_form_bloc.dart'
as _i510;
import 'package:enaklo/application/auth/logout_form/logout_form_bloc.dart'
as _i216;
import 'package:enaklo/application/auth/register_form/register_form_bloc.dart'
as _i260;
import 'package:enaklo/application/auth/resend_form/resend_form_bloc.dart'
@@ -24,6 +26,10 @@ import 'package:enaklo/application/auth/set_password/set_password_form_bloc.dart
as _i174;
import 'package:enaklo/application/auth/verify_form/verify_form_bloc.dart'
as _i521;
import 'package:enaklo/application/customer/customer_point_loader/customer_point_loader_bloc.dart'
as _i497;
import 'package:enaklo/application/game/ferris_wheel_loader/ferris_wheel_loader_bloc.dart'
as _i1013;
import 'package:enaklo/common/api/api_client.dart' as _i842;
import 'package:enaklo/common/di/di_auto_route.dart' as _i619;
import 'package:enaklo/common/di/di_connectivity.dart' as _i644;
@@ -31,6 +37,8 @@ import 'package:enaklo/common/di/di_dio.dart' as _i842;
import 'package:enaklo/common/di/di_shared_preferences.dart' as _i672;
import 'package:enaklo/common/network/network_client.dart' as _i109;
import 'package:enaklo/domain/auth/auth.dart' as _i995;
import 'package:enaklo/domain/customer/customer.dart' as _i898;
import 'package:enaklo/domain/game/game.dart' as _i96;
import 'package:enaklo/env.dart' as _i372;
import 'package:enaklo/infrastructure/auth/datasources/local_data_provider.dart'
as _i1003;
@@ -38,6 +46,14 @@ import 'package:enaklo/infrastructure/auth/datasources/remote_data_provider.dart
as _i818;
import 'package:enaklo/infrastructure/auth/repositories/auth_repository.dart'
as _i879;
import 'package:enaklo/infrastructure/customer/datasources/remote_data_provider.dart'
as _i89;
import 'package:enaklo/infrastructure/customer/repositories/customer_repository.dart'
as _i118;
import 'package:enaklo/infrastructure/game/datasources/remote_data_provider.dart'
as _i143;
import 'package:enaklo/infrastructure/game/repositories/game_repository.dart'
as _i547;
import 'package:enaklo/presentation/router/app_router.dart' as _i698;
import 'package:get_it/get_it.dart' as _i174;
import 'package:injectable/injectable.dart' as _i526;
@@ -78,6 +94,18 @@ extension GetItInjectableX on _i174.GetIt {
gh.factory<_i818.AuthRemoteDataProvider>(
() => _i818.AuthRemoteDataProvider(gh<_i842.ApiClient>()),
);
gh.factory<_i143.GameRemoteDataProvider>(
() => _i143.GameRemoteDataProvider(gh<_i842.ApiClient>()),
);
gh.factory<_i89.CustomerRemoteDataProvider>(
() => _i89.CustomerRemoteDataProvider(gh<_i842.ApiClient>()),
);
gh.factory<_i96.IGameRepository>(
() => _i547.GameRepository(gh<_i143.GameRemoteDataProvider>()),
);
gh.factory<_i1013.FerrisWheelLoaderBloc>(
() => _i1013.FerrisWheelLoaderBloc(gh<_i96.IGameRepository>()),
);
gh.factory<_i995.IAuthRepository>(
() => _i879.AuthRepository(
gh<_i818.AuthRemoteDataProvider>(),
@@ -96,15 +124,24 @@ extension GetItInjectableX on _i174.GetIt {
gh.factory<_i869.CheckPhoneFormBloc>(
() => _i869.CheckPhoneFormBloc(gh<_i995.IAuthRepository>()),
);
gh.factory<_i521.VerifyFormBloc>(
() => _i521.VerifyFormBloc(gh<_i995.IAuthRepository>()),
);
gh.factory<_i771.AuthBloc>(
() => _i771.AuthBloc(gh<_i995.IAuthRepository>()),
);
gh.factory<_i521.VerifyFormBloc>(
() => _i521.VerifyFormBloc(gh<_i995.IAuthRepository>()),
);
gh.factory<_i216.LogoutFormBloc>(
() => _i216.LogoutFormBloc(gh<_i995.IAuthRepository>()),
);
gh.factory<_i898.ICustomerRepository>(
() => _i118.CustomerRepository(gh<_i89.CustomerRemoteDataProvider>()),
);
gh.factory<_i510.LoginFormBloc>(
() => _i510.LoginFormBloc(gh<_i995.IAuthRepository>()),
);
gh.factory<_i497.CustomerPointLoaderBloc>(
() => _i497.CustomerPointLoaderBloc(gh<_i898.ICustomerRepository>()),
);
return this;
}
}
+7 -1
View File
@@ -2,6 +2,8 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../application/auth/auth_bloc.dart';
import '../application/auth/logout_form/logout_form_bloc.dart';
import '../application/customer/customer_point_loader/customer_point_loader_bloc.dart';
import '../common/theme/theme.dart';
import '../common/constant/app_constant.dart';
import '../injection.dart';
@@ -21,7 +23,11 @@ class _AppWidgetState extends State<AppWidget> {
@override
Widget build(BuildContext context) {
return MultiBlocProvider(
providers: [BlocProvider(create: (context) => getIt<AuthBloc>())],
providers: [
BlocProvider(create: (context) => getIt<AuthBloc>()),
BlocProvider(create: (context) => getIt<LogoutFormBloc>()),
BlocProvider(create: (context) => getIt<CustomerPointLoaderBloc>()),
],
child: MaterialApp.router(
debugShowCheckedModeBanner: false,
title: AppConstant.appName,
@@ -11,6 +11,52 @@
import 'package:flutter/widgets.dart';
class $AssetsAudioGen {
const $AssetsAudioGen();
/// File path: assets/audio/bell_ding.mp3
String get bellDing => 'assets/audio/bell_ding.mp3';
/// File path: assets/audio/big_win.mp3
String get bigWin => 'assets/audio/big_win.mp3';
/// File path: assets/audio/button_tap.mp3
String get buttonTap => 'assets/audio/button_tap.mp3';
/// File path: assets/audio/carnaval_main_theme.mp3
String get carnavalMainTheme => 'assets/audio/carnaval_main_theme.mp3';
/// File path: assets/audio/token_sound.mp3
String get tokenSound => 'assets/audio/token_sound.mp3';
/// File path: assets/audio/wheel_spin.mp3
String get wheelSpin => 'assets/audio/wheel_spin.mp3';
/// List of all assets
List<String> get values => [
bellDing,
bigWin,
buttonTap,
carnavalMainTheme,
tokenSound,
wheelSpin,
];
}
class $AssetsIconsGen {
const $AssetsIconsGen();
/// File path: assets/icons/dine_in.png
AssetGenImage get dineIn => const AssetGenImage('assets/icons/dine_in.png');
/// File path: assets/icons/takeaway.png
AssetGenImage get takeaway =>
const AssetGenImage('assets/icons/takeaway.png');
/// List of all assets
List<AssetGenImage> get values => [dineIn, takeaway];
}
class $AssetsImagesGen {
const $AssetsImagesGen();
@@ -64,6 +110,8 @@ class $AssetsImagesGen {
class Assets {
const Assets._();
static const $AssetsAudioGen audio = $AssetsAudioGen();
static const $AssetsIconsGen icons = $AssetsIconsGen();
static const $AssetsImagesGen images = $AssetsImagesGen();
}
@@ -0,0 +1,39 @@
import 'package:flutter/material.dart';
class DashedDivider extends StatelessWidget {
final double height;
final double dashWidth;
final double dashSpacing;
final Color color;
const DashedDivider({
super.key,
this.height = 1,
this.dashWidth = 5,
this.dashSpacing = 3,
this.color = Colors.grey,
});
@override
Widget build(BuildContext context) {
return SizedBox(
height: height,
child: LayoutBuilder(
builder: (context, constraints) {
final boxWidth = constraints.constrainWidth();
final dashCount = (boxWidth / (dashWidth + dashSpacing)).floor();
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: List.generate(dashCount, (_) {
return SizedBox(
width: dashWidth,
height: height,
child: DecoratedBox(decoration: BoxDecoration(color: color)),
);
}),
);
},
),
);
}
}
@@ -4,3 +4,5 @@ import 'package:flutter_spinkit/flutter_spinkit.dart';
import '../../../common/theme/theme.dart';
part 'elevated_button.dart';
part 'outline_button.dart';
part 'qty_button.dart';
@@ -6,7 +6,7 @@ class AppElevatedButton extends StatelessWidget {
required this.onPressed,
required this.title,
this.width = double.infinity,
this.height = 48.0,
this.height = 44.0,
this.isLoading = false,
});
@@ -41,7 +41,7 @@ class AppElevatedButton extends StatelessWidget {
)
: Text(
title,
style: AppStyle.lg.copyWith(
style: AppStyle.md.copyWith(
color: AppColor.white,
fontWeight: FontWeight.w700,
),
@@ -0,0 +1,58 @@
part of 'button.dart';
class AppOutlineButton extends StatelessWidget {
const AppOutlineButton({
super.key,
required this.onPressed,
required this.title,
this.width = double.infinity,
this.height = 44.0,
this.isLoading = false,
this.borderColor,
});
final Function()? onPressed;
final String title;
final double width;
final double height;
final bool isLoading;
final Color? borderColor;
@override
Widget build(BuildContext context) {
return SizedBox(
width: width,
height: height,
child: OutlinedButton(
onPressed: onPressed,
style: OutlinedButton.styleFrom(
padding: EdgeInsets.zero,
side: BorderSide(color: borderColor ?? AppColor.primary),
),
child: isLoading
? Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
SpinKitFadingCircle(color: AppColor.white, size: 24),
SizedBox(width: 8),
Text(
'Loading',
style: AppStyle.lg.copyWith(
color: AppColor.white,
fontWeight: FontWeight.w700,
),
),
],
)
: Text(
title,
style: AppStyle.md.copyWith(
color: AppColor.primary,
fontWeight: FontWeight.w700,
),
),
),
);
}
}
@@ -0,0 +1,38 @@
part of 'button.dart';
class QtyButton extends StatelessWidget {
const QtyButton({super.key});
@override
Widget build(BuildContext context) {
return Row(
children: [
InkWell(
child: Container(
height: 35,
width: 35,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(4),
border: Border.all(width: 1, color: AppColor.border),
),
child: Icon(Icons.remove),
),
),
SizedBox(width: 12),
Text('1', style: AppStyle.lg.copyWith(fontWeight: FontWeight.bold)),
SizedBox(width: 12),
InkWell(
child: Container(
height: 35,
width: 35,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(4),
border: Border.all(width: 1, color: AppColor.border),
),
child: Icon(Icons.add),
),
),
],
);
}
}
@@ -0,0 +1,118 @@
import 'package:flutter/material.dart';
import '../../../common/theme/theme.dart';
class GradientCard extends StatelessWidget {
final Widget child;
final List<Color>? gradientColors;
final double borderRadius;
final EdgeInsetsGeometry? padding;
final bool showDecoration;
const GradientCard({
super.key,
required this.child,
this.gradientColors,
this.borderRadius = 16,
this.padding = const EdgeInsets.all(16.0),
this.showDecoration = true,
});
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: gradientColors ?? AppColor.primaryGradient,
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(borderRadius),
),
child: Stack(
children: [
// Background Pattern (optional)
if (showDecoration) ..._buildDecorations(),
// Main Content
Padding(padding: padding ?? EdgeInsets.zero, child: child),
],
),
);
}
List<Widget> _buildDecorations() {
return [
// Top Right Circle
Positioned(
top: -20,
right: -20,
child: Container(
width: 80,
height: 80,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.white.withOpacity(0.1),
),
),
),
// Middle Right Circle
Positioned(
top: 30,
right: 20,
child: Container(
width: 40,
height: 40,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.white.withOpacity(0.08),
),
),
),
// Bottom Left Circle
Positioned(
bottom: -10,
left: -10,
child: Container(
width: 60,
height: 60,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.white.withOpacity(0.06),
),
),
),
// Top Left Decorative Line
Positioned(
top: 10,
left: -5,
child: Transform.rotate(
angle: 0.5,
child: Container(
width: 30,
height: 2,
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.15),
borderRadius: BorderRadius.circular(1),
),
),
),
),
// Bottom Right Decorative Line
Positioned(
bottom: 15,
right: 10,
child: Transform.rotate(
angle: -0.5,
child: Container(
width: 25,
height: 2,
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.15),
borderRadius: BorderRadius.circular(1),
),
),
),
),
];
}
}
@@ -0,0 +1,153 @@
import 'package:flutter/material.dart';
import '../../../common/extension/extension.dart';
import '../../../common/theme/theme.dart';
import '../../../sample/product_sample_data.dart';
class ProductCard extends StatelessWidget {
final Product product;
final Function()? onTap;
const ProductCard({super.key, required this.product, this.onTap});
@override
Widget build(BuildContext context) {
return InkWell(
onTap: onTap,
child: Container(
decoration: BoxDecoration(
color: AppColor.surface,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: AppColor.black.withOpacity(0.06),
blurRadius: 8,
offset: Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Product image
Expanded(
flex: 3,
child: Stack(
children: [
Container(
width: double.infinity,
decoration: BoxDecoration(
color: AppColor.backgroundLight,
borderRadius: BorderRadius.vertical(
top: Radius.circular(12),
),
),
child: Center(
child: Icon(
Icons.fastfood,
size: 40,
color: AppColor.textLight,
),
),
),
// Availability overlay
if (!product.isAvailable)
Container(
width: double.infinity,
height: double.infinity,
decoration: BoxDecoration(
color: AppColor.black.withOpacity(0.6),
borderRadius: BorderRadius.vertical(
top: Radius.circular(12),
),
),
child: Center(
child: Text(
"HABIS",
style: AppStyle.sm.copyWith(
color: AppColor.textWhite,
fontWeight: FontWeight.bold,
),
),
),
),
// Rating badge
Positioned(
top: 8,
right: 8,
child: Container(
padding: EdgeInsets.symmetric(horizontal: 6, vertical: 3),
decoration: BoxDecoration(
color: AppColor.surface,
borderRadius: BorderRadius.circular(8),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.star, size: 12, color: AppColor.warning),
SizedBox(width: 2),
Text(
"${product.rating}",
style: AppStyle.xs.copyWith(
color: AppColor.textPrimary,
fontWeight: FontWeight.w600,
),
),
],
),
),
),
],
),
),
// Product info
Expanded(
flex: 2,
child: Padding(
padding: EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
product.name,
style: AppStyle.md.copyWith(
fontWeight: FontWeight.bold,
color: AppColor.textPrimary,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
Spacer(),
// Price and sold count
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Rp ${product.price.currencyFormatRp}",
style: AppStyle.md.copyWith(
fontWeight: FontWeight.bold,
color: AppColor.primary,
),
),
Text(
"${product.soldCount} terjual",
style: AppStyle.xs.copyWith(
color: AppColor.textSecondary,
),
),
],
),
],
),
),
),
],
),
),
);
}
}
@@ -0,0 +1,36 @@
import 'package:flutter/material.dart';
import '../../../common/theme/theme.dart';
class ProductEmptyCard extends StatelessWidget {
const ProductEmptyCard({super.key});
@override
Widget build(BuildContext context) {
return Container(
margin: EdgeInsets.symmetric(horizontal: 16),
padding: EdgeInsets.all(24),
decoration: BoxDecoration(
color: AppColor.surface,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: AppColor.borderLight),
),
child: Center(
child: Column(
children: [
Icon(
Icons.inventory_2_outlined,
size: 48,
color: AppColor.textLight,
),
SizedBox(height: 12),
Text(
"Belum ada produk",
style: AppStyle.md.copyWith(color: AppColor.textSecondary),
),
],
),
),
);
}
}
@@ -0,0 +1,63 @@
import 'package:flutter/material.dart';
import '../../../common/data/service_data.dart';
import '../../../common/theme/theme.dart';
import '../image/image.dart';
import 'gradient_card.dart';
class ServiceCard extends StatelessWidget {
final Service service;
const ServiceCard({super.key, required this.service});
@override
Widget build(BuildContext context) {
return GradientCard(
padding: EdgeInsets.symmetric(vertical: 12, horizontal: 16),
child: Row(
children: [
ClipRRect(
borderRadius: BorderRadiusGeometry.circular(8),
child: Image.asset(
service.imagePath,
width: 60,
height: 60,
errorBuilder: (context, error, stackTrace) =>
ImagePlaceholder(width: 60, height: 60),
),
),
SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
service.name,
style: AppStyle.xl.copyWith(
fontWeight: FontWeight.bold,
color: AppColor.white,
),
),
Text(
service.description,
style: AppStyle.md.copyWith(
fontWeight: FontWeight.w500,
color: AppColor.white,
),
),
],
),
),
InkWell(
child: Text(
'Ubah',
style: AppStyle.md.copyWith(
color: AppColor.white,
fontWeight: FontWeight.bold,
),
),
),
],
),
);
}
}
@@ -0,0 +1,43 @@
import 'package:flutter/material.dart';
import '../../../common/extension/extension.dart';
import '../../../common/theme/theme.dart';
class VariantCard extends StatelessWidget {
final String name;
final bool isSelected;
const VariantCard({super.key, required this.name, this.isSelected = false});
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.symmetric(vertical: 16, horizontal: 8),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(AppValue.borderRadius),
color: isSelected ? AppColor.primary.withOpacity(0.1) : AppColor.white,
border: Border.all(
width: isSelected ? 2 : 1,
color: isSelected ? AppColor.primary : AppColor.border,
),
),
child: Row(
children: [
Expanded(
child: Text(
name,
style: AppStyle.md.copyWith(fontWeight: FontWeight.w600),
),
),
SizedBox(width: 12),
Text(
"+${"2000".currencyFormatRp}",
style: AppStyle.md.copyWith(
color: AppColor.primary,
fontWeight: FontWeight.w500,
),
),
],
),
);
}
}
@@ -0,0 +1,106 @@
import 'package:flutter/widgets.dart';
import '../../../common/theme/theme.dart';
import '../../../sample/product_sample_data.dart';
class SliverCategoryDelegate extends SliverPersistentHeaderDelegate {
final List<ProductCategory> categories;
final String selectedCategoryId;
final Function(String) onCategoryTap;
SliverCategoryDelegate({
required this.categories,
required this.selectedCategoryId,
required this.onCategoryTap,
});
@override
double get minExtent => 60;
@override
double get maxExtent => 60;
@override
Widget build(
BuildContext context,
double shrinkOffset,
bool overlapsContent,
) {
return Container(
height: 60,
decoration: BoxDecoration(
color: AppColor.surface,
border: Border(
bottom: BorderSide(color: AppColor.borderLight, width: 1),
),
),
child: ListView.builder(
scrollDirection: Axis.horizontal,
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
itemCount: categories.length,
itemBuilder: (context, index) {
final category = categories[index];
final isSelected = category.id == selectedCategoryId;
return GestureDetector(
onTap: () => onCategoryTap(category.id),
child: Container(
margin: EdgeInsets.only(right: 12),
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: isSelected ? AppColor.primary : AppColor.backgroundLight,
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: isSelected ? AppColor.primary : AppColor.border,
width: 1,
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(category.icon, style: AppStyle.md),
SizedBox(width: 8),
Text(
category.name,
style: AppStyle.md.copyWith(
color: isSelected
? AppColor.textWhite
: AppColor.textPrimary,
fontWeight: FontWeight.w600,
),
),
SizedBox(width: 4),
Container(
padding: EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: isSelected
? AppColor.textWhite.withOpacity(0.2)
: AppColor.primary.withOpacity(0.1),
borderRadius: BorderRadius.circular(10),
),
child: Text(
"${category.productCount}",
style: AppStyle.xs.copyWith(
color: isSelected
? AppColor.textWhite
: AppColor.primary,
fontWeight: FontWeight.bold,
),
),
),
],
),
),
);
},
),
);
}
@override
bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) {
return oldDelegate is SliverCategoryDelegate &&
(oldDelegate.selectedCategoryId != selectedCategoryId ||
oldDelegate.categories != categories);
}
}
@@ -0,0 +1,225 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import '../../../common/data/service_data.dart';
import '../../../common/extension/extension.dart';
import '../../../common/theme/theme.dart';
import '../../../sample/product_sample_data.dart';
import '../../components/border/dashed_border.dart';
import '../../components/button/button.dart';
import '../../components/card/service_card.dart';
import 'widgets/checkout_item.dart';
import 'widgets/merchant.dart';
@RoutePage()
class CheckoutPage extends StatelessWidget {
const CheckoutPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Checkout')),
bottomNavigationBar: Container(
padding: EdgeInsets.all(AppValue.padding).copyWith(bottom: 24),
decoration: BoxDecoration(
color: AppColor.white,
boxShadow: [
BoxShadow(
color: AppColor.black.withOpacity(0.1),
offset: Offset(2, 0),
blurRadius: 10,
),
],
),
child: AppElevatedButton(onPressed: () {}, title: 'Pesan Sekarang'),
),
body: ListView(
children: [
Container(
padding: EdgeInsets.all(AppValue.padding),
child: Column(
children: [
ServiceCard(service: services.first),
SizedBox(height: 16),
CheckoutMerchant(),
],
),
),
Divider(thickness: 4, color: AppColor.borderLight),
Container(
padding: EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_sectionTitle('Pesananmu'),
SizedBox(height: 16),
CheckoutItem(product: products.first),
CheckoutItem(product: products.first),
Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Ada Tambah lagi?',
style: AppStyle.md.copyWith(
fontWeight: FontWeight.w600,
),
),
SizedBox(height: 4),
Text(
'Kakmu bisa menambah menu lainnya, ya.',
style: AppStyle.md,
),
],
),
),
SizedBox(width: 12),
AppOutlineButton(
width: 100,
onPressed: () {},
title: '+ Tambah',
),
],
),
],
),
),
Divider(thickness: 4, color: AppColor.borderLight),
Container(
padding: EdgeInsets.all(AppValue.padding),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_sectionTitle('Voucher'),
SizedBox(height: 16),
Row(
children: [
Container(
padding: EdgeInsets.all(4),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(4),
color: AppColor.primary.withOpacity(0.1),
),
child: Icon(
Icons.confirmation_number,
color: AppColor.primary,
),
),
SizedBox(width: 12),
Expanded(
child: Text(
'Kamu bisa menggunakan voucher mu disini.',
style: AppStyle.md,
),
),
Icon(Icons.chevron_right, color: AppColor.textSecondary),
],
),
],
),
),
Divider(thickness: 4, color: AppColor.borderLight),
Container(
padding: EdgeInsets.all(AppValue.padding),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_sectionTitle('Metode Pembayaran'),
SizedBox(height: 16),
Row(
children: [
Container(
padding: EdgeInsets.all(4),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(4),
color: AppColor.primary.withOpacity(0.1),
),
child: Icon(Icons.wallet, color: AppColor.primary),
),
SizedBox(width: 12),
Expanded(
child: Text(
'Pilih metode pembayaran disini.',
style: AppStyle.md,
),
),
Icon(Icons.chevron_right, color: AppColor.textSecondary),
],
),
],
),
),
Divider(thickness: 4, color: AppColor.borderLight),
Container(
padding: EdgeInsets.all(AppValue.padding),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_sectionTitle('Rincian Pembayaran'),
SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Harga',
style: AppStyle.md.copyWith(fontWeight: FontWeight.w500),
),
Text(
"40000".currencyFormatRp,
style: AppStyle.md.copyWith(fontWeight: FontWeight.w500),
),
],
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 12),
child: DashedDivider(),
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Total Pembayaran',
style: AppStyle.lg.copyWith(fontWeight: FontWeight.bold),
),
Text(
"40000".currencyFormatRp,
style: AppStyle.lg.copyWith(fontWeight: FontWeight.bold),
),
],
),
],
),
),
Divider(thickness: 4, color: AppColor.borderLight),
Container(
padding: EdgeInsets.all(AppValue.padding),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Kebijakan Pembatalan",
style: AppStyle.md.copyWith(fontWeight: FontWeight.bold),
),
SizedBox(height: 4),
Text(
'Kamu tidak dapat melakukan pembatalan atau perubahan apapun pada pesanan setelah melakukan pembayaran.',
style: AppStyle.sm,
),
],
),
),
SizedBox(height: 40),
],
),
);
}
Text _sectionTitle(String title) {
return Text(
title,
style: AppStyle.xl.copyWith(fontWeight: FontWeight.bold),
);
}
}
@@ -0,0 +1,68 @@
import 'package:flutter/material.dart';
import '../../../../common/extension/extension.dart';
import '../../../../common/theme/theme.dart';
import '../../../../sample/product_sample_data.dart';
import '../../../components/button/button.dart';
import '../../../components/image/image.dart';
class CheckoutItem extends StatelessWidget {
final Product product;
const CheckoutItem({super.key, required this.product});
@override
Widget build(BuildContext context) {
return Container(
margin: EdgeInsets.only(bottom: 16),
decoration: BoxDecoration(),
child: Column(
children: [
Row(
children: [
AppNetworkImage(url: product.imageUrl, width: 60, height: 60),
SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
product.name,
style: AppStyle.md.copyWith(fontWeight: FontWeight.bold),
),
SizedBox(height: 4),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"1x ${product.price.currencyFormatRp}",
style: AppStyle.md,
),
Text("27000".currencyFormatRp, style: AppStyle.md),
],
),
],
),
),
],
),
SizedBox(height: 8),
Row(
children: [
Expanded(
child: Text(
'Catatan',
style: AppStyle.md.copyWith(
color: AppColor.textSecondary,
fontWeight: FontWeight.w600,
),
),
),
SizedBox(width: 12),
QtyButton(),
],
),
],
),
);
}
}
@@ -0,0 +1,44 @@
import 'package:flutter/material.dart';
import '../../../../common/constant/app_constant.dart';
import '../../../../common/theme/theme.dart';
class CheckoutMerchant extends StatelessWidget {
const CheckoutMerchant({super.key});
@override
Widget build(BuildContext context) {
return Row(
children: [
Container(
width: 60,
height: 60,
decoration: BoxDecoration(
color: AppColor.surface,
borderRadius: BorderRadius.circular(12),
),
child: Center(
child: ClipRRect(
borderRadius: BorderRadius.circular(12),
child: Image.asset(merchant.imageUrl),
),
),
),
SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
merchant.name,
style: AppStyle.h6.copyWith(fontWeight: FontWeight.bold),
),
Text(merchant.category, style: AppStyle.md.copyWith()),
Row(children: [SizedBox(width: 12)]),
],
),
),
],
);
}
}
@@ -1,6 +1,8 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../application/customer/customer_point_loader/customer_point_loader_bloc.dart';
import '../../../common/theme/theme.dart';
import '../../router/app_router.gr.dart';
@@ -58,14 +60,14 @@ class Product {
}
@RoutePage()
class PoinPage extends StatefulWidget {
const PoinPage({super.key});
class CoinPage extends StatefulWidget {
const CoinPage({super.key});
@override
State<PoinPage> createState() => _PoinPageState();
State<CoinPage> createState() => _CoinPageState();
}
class _PoinPageState extends State<PoinPage> {
class _CoinPageState extends State<CoinPage> {
final ScrollController _scrollController = ScrollController();
// Sample data - Indonesian content
@@ -201,6 +203,9 @@ class _PoinPageState extends State<PoinPage> {
super.initState();
activeCategoryId = categories.first.id; // Set first category as active
_initializeCategoryKeys();
context.read<CustomerPointLoaderBloc>().add(
CustomerPointLoaderEvent.fetched(),
);
}
void _initializeCategoryKeys() {
@@ -271,7 +276,7 @@ class _PoinPageState extends State<PoinPage> {
actions: [
IconButton(
icon: Icon(Icons.history),
onPressed: () => context.router.push(PoinHistoryRoute()),
onPressed: () => context.router.push(CoinHistoryRoute()),
),
],
),
@@ -304,6 +309,8 @@ class _PoinPageState extends State<PoinPage> {
}
Widget _buildPointCard() {
return BlocBuilder<CustomerPointLoaderBloc, CustomerPointLoaderState>(
builder: (context, state) {
return Container(
margin: EdgeInsets.all(16),
padding: EdgeInsets.all(20),
@@ -331,16 +338,16 @@ class _PoinPageState extends State<PoinPage> {
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Text(
// "",
// style: AppStyle.sm.copyWith(
// color: AppColor.textWhite.withOpacity(0.9),
// fontWeight: FontWeight.w500,
// ),
// ),
// SizedBox(height: 4),
Text(
pointCard.membershipLevel,
style: AppStyle.sm.copyWith(
color: AppColor.textWhite.withOpacity(0.9),
fontWeight: FontWeight.w500,
),
),
SizedBox(height: 4),
Text(
"${pointCard.availablePoints}",
"${state.customerPoint.totalPoints}",
style: AppStyle.h2.copyWith(
color: AppColor.textWhite,
fontWeight: FontWeight.w700,
@@ -368,47 +375,50 @@ class _PoinPageState extends State<PoinPage> {
),
],
),
SizedBox(height: 16),
Container(
height: 8,
decoration: BoxDecoration(
color: AppColor.white.withOpacity(0.3),
borderRadius: BorderRadius.circular(4),
),
child: FractionallySizedBox(
widthFactor:
(pointCard.totalPoints - pointCard.usedPoints) /
pointCard.totalPoints,
alignment: Alignment.centerLeft,
child: Container(
decoration: BoxDecoration(
color: AppColor.textWhite,
borderRadius: BorderRadius.circular(4),
),
),
),
),
SizedBox(height: 8),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Terpakai: ${pointCard.usedPoints}",
style: AppStyle.xs.copyWith(
color: AppColor.textWhite.withOpacity(0.8),
),
),
Text(
"Total: ${pointCard.totalPoints}",
style: AppStyle.xs.copyWith(
color: AppColor.textWhite.withOpacity(0.8),
),
),
],
),
// SizedBox(height: 16),
// Container(
// height: 8,
// decoration: BoxDecoration(
// color: AppColor.white.withOpacity(0.3),
// borderRadius: BorderRadius.circular(4),
// ),
// child: FractionallySizedBox(
// widthFactor:
// (pointCard.totalPoints - pointCard.usedPoints) /
// pointCard.totalPoints,
// alignment: Alignment.centerLeft,
// child: Container(
// decoration: BoxDecoration(
// color: AppColor.textWhite,
// borderRadius: BorderRadius.circular(4),
// ),
// ),
// ),
// ),
// SizedBox(height: 8),
// Row(
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
// children: [
// Text(
// "Terpakai: ${pointCard.usedPoints}",
// style: AppStyle.xs.copyWith(
// color: AppColor.textWhite.withOpacity(0.8),
// ),
// ),
// Text(
// "Total: ${pointCard.totalPoints}",
// style: AppStyle.xs.copyWith(
// color: AppColor.textWhite.withOpacity(0.8),
// ),
// ),
// ],
// ),
],
),
);
},
);
}
Widget _buildCategoryTabs() {
@@ -43,14 +43,14 @@ class PointTransaction {
}
@RoutePage()
class PoinHistoryPage extends StatefulWidget {
const PoinHistoryPage({super.key});
class CoinHistoryPage extends StatefulWidget {
const CoinHistoryPage({super.key});
@override
State<PoinHistoryPage> createState() => _PoinHistoryPageState();
State<CoinHistoryPage> createState() => _CoinHistoryPageState();
}
class _PoinHistoryPageState extends State<PoinHistoryPage> {
class _CoinHistoryPageState extends State<CoinHistoryPage> {
TransactionType selectedFilter = TransactionType.all;
// Sample transaction data
@@ -2,7 +2,7 @@ import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import '../../../../../common/theme/theme.dart';
import '../../poin_page.dart';
import '../../coin_page.dart';
@RoutePage()
class ProductRedeemPage extends StatefulWidget {
+20 -2
View File
@@ -1,17 +1,35 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../application/auth/auth_bloc.dart';
import '../../router/app_router.gr.dart';
import 'widgets/bottom_navbar.dart';
@RoutePage()
class MainPage extends StatelessWidget {
class MainPage extends StatefulWidget {
const MainPage({super.key});
@override
State<MainPage> createState() => _MainPageState();
}
class _MainPageState extends State<MainPage> {
@override
initState() {
super.initState();
context.read<AuthBloc>().add(const AuthEvent.fetchCurrentUser());
}
@override
Widget build(BuildContext context) {
return AutoTabsRouter.pageView(
routes: [HomeRoute(), VoucherRoute(), OrderRoute(), ProfileRoute()],
routes: [
HomeRoute(),
// VoucherRoute(),
OrderRoute(),
ProfileRoute(),
],
physics: const NeverScrollableScrollPhysics(),
builder: (context, child, pageController) => Scaffold(
body: child,
@@ -1,14 +1,18 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:carousel_slider/carousel_slider.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../../../application/auth/auth_bloc.dart';
import '../../../../../application/customer/customer_point_loader/customer_point_loader_bloc.dart';
import '../../../../../common/constant/app_constant.dart';
import '../../../../../common/theme/theme.dart';
import '../../../../components/image/image.dart';
import '../../../../router/app_router.gr.dart';
import 'widgets/feature_section.dart';
import 'widgets/lottery_card.dart';
import 'widgets/banner_card.dart';
import 'widgets/point_card.dart';
import 'widgets/popular_merchant_section.dart';
import 'widgets/service_section.dart';
@RoutePage()
class HomePage extends StatefulWidget {
@@ -30,6 +34,16 @@ class _HomePageState extends State<HomePage> {
'https://images.unsplash.com/photo-1574848794584-c740d6a5595f?w=800&h=400&fit=crop',
];
@override
void initState() {
super.initState();
if (context.read<AuthBloc>().state.isAuthenticated) {
context.read<CustomerPointLoaderBloc>().add(
CustomerPointLoaderEvent.fetched(),
);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
@@ -39,8 +53,14 @@ class _HomePageState extends State<HomePage> {
children: [
_buildHeaderSection(),
const SizedBox(height: 70),
HomeFeatureSection(),
HomeLotteryBanner(onTap: () => context.router.push(DrawRoute())),
HomeServiceSection(),
HomeBanner(
title: '🎰 My Rewards',
subtitle:
'Main dan tepat menangkan produk gratis dari ${AppConstant.appName}.',
actionText: 'Mainkan Sekarang',
onTap: () => context.router.push(DrawRoute()),
),
HomePopularMerchantSection(),
],
),
@@ -61,7 +81,19 @@ class _HomePageState extends State<HomePage> {
top: 225,
child: _buildCarouselIndicators(),
),
Positioned(left: 16, right: 16, top: 240, child: HomePointCard()),
Positioned(
left: 16,
right: 16,
top: 240,
child: HomePointCard(
point: context
.read<CustomerPointLoaderBloc>()
.state
.customerPoint
.totalPoints
.toString(),
),
),
],
);
}
@@ -1,8 +1,8 @@
import 'package:flutter/material.dart';
import '../../../../../../common/theme/theme.dart';
class HomeLotteryBanner extends StatefulWidget {
const HomeLotteryBanner({
class HomeBanner extends StatefulWidget {
const HomeBanner({
super.key,
this.onTap,
this.title = "🎰 UNDIAN BERHADIAH",
@@ -18,11 +18,10 @@ class HomeLotteryBanner extends StatefulWidget {
final String actionText;
@override
State<HomeLotteryBanner> createState() => _HomeLotteryBannerState();
State<HomeBanner> createState() => _HomeBannerState();
}
class _HomeLotteryBannerState extends State<HomeLotteryBanner>
with TickerProviderStateMixin {
class _HomeBannerState extends State<HomeBanner> with TickerProviderStateMixin {
late AnimationController _pulseController;
late AnimationController _shimmerController;
late AnimationController _floatingController;
@@ -290,7 +289,7 @@ class _HomeLotteryBannerState extends State<HomeLotteryBanner>
children: [
const Center(
child: Icon(
Icons.casino,
Icons.games,
color: Colors.white,
size: 32,
shadows: [
@@ -1,44 +1,60 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import '../../../../../router/app_router.gr.dart';
import 'feature_card.dart';
import '../../../../../../common/theme/theme.dart';
import '../../../../../components/card/gradient_card.dart';
class HomeFeatureSection extends StatelessWidget {
const HomeFeatureSection({super.key});
@override
Widget build(BuildContext context) {
return Container(
return Padding(
padding: const EdgeInsets.all(16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
HomeFeatureCard(
icon: Icons.card_giftcard,
title: 'Reward',
iconColor: const Color(0xFF1976D2),
onTap: () => context.router.push(RewardRoute()),
Expanded(
child: GradientCard(
child: _content(
'Dine In',
'Rasakan Sensasi Langsung di Meja Kami!',
),
HomeFeatureCard(
icon: Icons.casino,
title: 'Undian',
iconColor: const Color(0xFF7B1FA2),
onTap: () => context.router.push(DrawRoute()),
),
HomeFeatureCard(
icon: Icons.store,
title: 'Merchant',
iconColor: const Color(0xFF388E3C),
onTap: () => context.router.push(MerchantRoute()),
),
HomeFeatureCard(
icon: Icons.blur_circular,
title: 'Wheels',
iconColor: const Color(0xFF388E3C),
onTap: () => context.router.push(FerrisWheelRoute()),
SizedBox(width: 16),
Expanded(
child: GradientCard(
child: _content(
'Take Away',
'Nikmati di Mana Saja, Tetap Mantap!',
),
),
),
],
),
);
}
Column _content(String title, String subtitle) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: AppStyle.lg.copyWith(
color: AppColor.white,
fontWeight: FontWeight.bold,
),
),
SizedBox(height: 8),
Text(
subtitle,
style: AppStyle.md.copyWith(
color: AppColor.white,
fontWeight: FontWeight.w500,
),
),
],
);
}
}
@@ -1,16 +1,23 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../../../../application/auth/auth_bloc.dart';
import '../../../../../../common/theme/theme.dart';
import '../../../../../router/app_router.gr.dart';
class HomePointCard extends StatelessWidget {
const HomePointCard({super.key});
final String point;
const HomePointCard({super.key, required this.point});
@override
Widget build(BuildContext context) {
return BlocBuilder<AuthBloc, AuthState>(
builder: (context, state) {
return GestureDetector(
onTap: () => context.router.push(PoinRoute()),
onTap: () => state.isAuthenticated
? context.router.push(CoinRoute())
: context.router.push(OnboardingRoute()),
child: Container(
decoration: BoxDecoration(
color: AppColor.white,
@@ -33,8 +40,20 @@ class HomePointCard extends StatelessWidget {
children: [
Row(
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
!state.isAuthenticated
? Expanded(
child: Text(
'Hi, Selamat Datang Di Enaklo',
style: AppStyle.md.copyWith(
color: AppColor.primary,
fontWeight: FontWeight.w600,
),
),
)
: Expanded(
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Container(
padding: const EdgeInsets.symmetric(
@@ -43,7 +62,9 @@ class HomePointCard extends StatelessWidget {
),
decoration: BoxDecoration(
color: AppColor.primary,
borderRadius: BorderRadius.circular(25),
borderRadius: BorderRadius.circular(
25,
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
@@ -55,7 +76,7 @@ class HomePointCard extends StatelessWidget {
),
SizedBox(width: 8),
Text(
'148 Poin',
'$point Poin',
style: AppStyle.md.copyWith(
color: AppColor.white,
fontWeight: FontWeight.w600,
@@ -66,7 +87,7 @@ class HomePointCard extends StatelessWidget {
),
const SizedBox(height: 4),
Text(
'Kamu punya 148 poin',
'Kamu punya $point poin',
style: AppStyle.sm.copyWith(
color: AppColor.textSecondary,
fontSize: 11,
@@ -74,7 +95,8 @@ class HomePointCard extends StatelessWidget {
),
],
),
const Spacer(),
),
SizedBox(
width: 120,
height: 40,
@@ -119,7 +141,9 @@ class HomePointCard extends StatelessWidget {
Row(
children: [
Text(
'Tukarkan poinmu dengan hadiah menarik',
state.isAuthenticated
? 'Tukarkan poinmu dengan hadiah menarik'
: 'Silahkan login untuk tukarkan poinmu',
style: AppStyle.sm.copyWith(
color: AppColor.textPrimary,
fontSize: 13,
@@ -141,6 +165,8 @@ class HomePointCard extends StatelessWidget {
),
),
);
},
);
}
Widget _buildCoinPattern() {
@@ -0,0 +1,71 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import '../../../../../../common/data/service_data.dart';
import '../../../../../../common/theme/theme.dart';
import '../../../../../components/card/gradient_card.dart';
import '../../../../../router/app_router.gr.dart';
class HomeServiceSection extends StatelessWidget {
const HomeServiceSection({super.key});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(16),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: List.generate(services.length, (index) {
return Expanded(
child: Padding(
padding: EdgeInsets.only(right: index == 0 ? 12 : 0),
child: InkWell(
onTap: () =>
context.router.push(MenuRoute(service: services[index])),
child: GradientCard(
padding: EdgeInsets.zero,
child: _content(services[index]),
),
),
),
);
}),
),
);
}
Stack _content(Service service) {
return Stack(
children: [
Positioned(
right: 4,
top: 4,
child: Image.asset(service.imagePath, width: 60, height: 60),
),
Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
service.name,
style: AppStyle.lg.copyWith(
color: AppColor.white,
fontWeight: FontWeight.bold,
),
),
SizedBox(height: 8),
Text(
service.description,
style: AppStyle.md.copyWith(
color: AppColor.white,
fontWeight: FontWeight.w500,
),
),
],
),
),
],
);
}
}
@@ -1,19 +1,38 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../../../application/auth/auth_bloc.dart';
import '../../../../../application/auth/logout_form/logout_form_bloc.dart';
import '../../../../../common/theme/theme.dart';
import '../../../../../injection.dart';
import '../../../../components/card/gradient_card.dart';
import '../../../../components/toast/flushbar.dart';
import '../../../../router/app_router.gr.dart';
@RoutePage()
class ProfilePage extends StatelessWidget {
class ProfilePage extends StatelessWidget implements AutoRouteWrapper {
const ProfilePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
return BlocListener<LogoutFormBloc, LogoutFormState>(
listener: (context, state) {
state.failureOrAuthOption.fold(
() => null,
(either) => either.fold(
(f) => AppFlushbar.showAuthFailureToast(context, f),
(_) => context.router.replaceAll([OnboardingRoute()]),
),
);
},
child: Scaffold(
backgroundColor: AppColor.background,
appBar: AppBar(title: Text('Profil'), automaticallyImplyLeading: false),
body: SingleChildScrollView(
body: BlocBuilder<AuthBloc, AuthState>(
builder: (context, state) {
return SingleChildScrollView(
child: Column(
children: [
// Profile Header
@@ -25,90 +44,26 @@ class ProfilePage extends StatelessWidget {
children: [
// Profile Avatar & Info
GestureDetector(
onTap: () => context.router.push(AccountMyRoute()),
child: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: AppColor.primaryGradient,
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(16),
),
child: Stack(
onTap: () => state.isAuthenticated
? context.router.push(AccountMyRoute())
: context.router.push(OnboardingRoute()),
child: GradientCard(
child: !state.isAuthenticated
? Row(
children: [
// Background Pattern
Positioned(
top: -20,
right: -20,
child: Container(
width: 80,
height: 80,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: AppColor.white.withOpacity(0.1),
Expanded(
child: Text(
'Silahkan Masuk',
style: AppStyle.lg.copyWith(
fontWeight: FontWeight.bold,
color: AppColor.white,
letterSpacing: 0.5,
),
),
),
Positioned(
top: 30,
right: 20,
child: Container(
width: 40,
height: 40,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: AppColor.white.withOpacity(0.08),
),
),
),
Positioned(
bottom: -10,
left: -10,
child: Container(
width: 60,
height: 60,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: AppColor.white.withOpacity(0.06),
),
),
),
// Decorative Lines
Positioned(
top: 10,
left: -5,
child: Transform.rotate(
angle: 0.5,
child: Container(
width: 30,
height: 2,
decoration: BoxDecoration(
color: AppColor.white.withOpacity(0.15),
borderRadius: BorderRadius.circular(1),
),
),
),
),
Positioned(
bottom: 15,
right: 10,
child: Transform.rotate(
angle: -0.5,
child: Container(
width: 25,
height: 2,
decoration: BoxDecoration(
color: AppColor.white.withOpacity(0.15),
borderRadius: BorderRadius.circular(1),
),
),
),
),
// Main Content
Padding(
padding: const EdgeInsets.all(16.0),
child: Row(
],
)
: Row(
children: [
// Avatar
Container(
@@ -119,7 +74,9 @@ class ProfilePage extends StatelessWidget {
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: AppColor.black.withOpacity(0.1),
color: AppColor.black.withOpacity(
0.1,
),
blurRadius: 8,
offset: const Offset(0, 2),
),
@@ -139,7 +96,7 @@ class ProfilePage extends StatelessWidget {
CrossAxisAlignment.start,
children: [
Text(
'EFRIL',
state.user.name,
style: AppStyle.lg.copyWith(
fontWeight: FontWeight.bold,
color: AppColor.white,
@@ -148,11 +105,10 @@ class ProfilePage extends StatelessWidget {
),
const SizedBox(height: 4),
Text(
'+6283873987851',
state.user.phoneNumber,
style: AppStyle.sm.copyWith(
color: AppColor.white.withOpacity(
0.9,
),
color: AppColor.white
.withOpacity(0.9),
),
),
],
@@ -167,9 +123,6 @@ class ProfilePage extends StatelessWidget {
],
),
),
],
),
),
),
],
),
@@ -178,6 +131,7 @@ class ProfilePage extends StatelessWidget {
const SizedBox(height: 8),
// Menu Items
if (state.isAuthenticated) ...[
Container(
color: AppColor.white,
child: Column(
@@ -206,8 +160,8 @@ class ProfilePage extends StatelessWidget {
],
),
),
const SizedBox(height: 8),
],
// Legal & Privacy Section
Container(
@@ -248,7 +202,9 @@ class ProfilePage extends StatelessWidget {
const SizedBox(height: 8),
Text(
'Customer Service kami siap untuk membantu',
style: AppStyle.sm.copyWith(color: AppColor.textSecondary),
style: AppStyle.sm.copyWith(
color: AppColor.textSecondary,
),
),
const SizedBox(height: 16),
// WhatsApp Customer Service
@@ -310,6 +266,7 @@ class ProfilePage extends StatelessWidget {
const SizedBox(height: 20),
// Footer Section
if (state.isAuthenticated)
Container(
color: AppColor.white,
padding: const EdgeInsets.all(20),
@@ -318,10 +275,17 @@ class ProfilePage extends StatelessWidget {
children: [
Text(
'Version 4.6.1',
style: AppStyle.sm.copyWith(color: AppColor.textSecondary),
style: AppStyle.sm.copyWith(
color: AppColor.textSecondary,
),
),
GestureDetector(
onTap: () => _showLogoutDialog(context),
onTap: () => _showLogoutDialog(
context,
onLogout: () => context
.read<LogoutFormBloc>()
.add(const LogoutFormEvent.submitted()),
),
child: Text(
'Logout',
style: AppStyle.sm.copyWith(
@@ -337,6 +301,9 @@ class ProfilePage extends StatelessWidget {
const SizedBox(height: 100), // Bottom spacing
],
),
);
},
),
),
);
}
@@ -372,7 +339,10 @@ class ProfilePage extends StatelessWidget {
);
}
void _showLogoutDialog(BuildContext context) {
void _showLogoutDialog(
BuildContext context, {
required VoidCallback onLogout,
}) {
showDialog(
context: context,
builder: (BuildContext context) {
@@ -395,6 +365,7 @@ class ProfilePage extends StatelessWidget {
onPressed: () => Navigator.of(context).pop(),
),
TextButton(
onPressed: onLogout,
child: Text(
'Logout',
style: AppStyle.md.copyWith(
@@ -402,15 +373,14 @@ class ProfilePage extends StatelessWidget {
fontWeight: FontWeight.w600,
),
),
onPressed: () {
Navigator.of(context).pop();
// Add logout logic here
// Example: context.router.pushAndClearStack('/login');
},
),
],
);
},
);
}
@override
Widget wrappedRoute(BuildContext context) =>
BlocProvider(create: (_) => getIt<LogoutFormBloc>(), child: this);
}
@@ -18,11 +18,11 @@ class MainBottomNavbar extends StatelessWidget {
label: 'Home',
tooltip: 'Home',
),
BottomNavigationBarItem(
icon: Icon(Icons.discount),
label: 'Voucher',
tooltip: 'Voucher',
),
// BottomNavigationBarItem(
// icon: Icon(Icons.discount),
// label: 'Voucher',
// tooltip: 'Voucher',
// ),
BottomNavigationBarItem(
icon: Icon(Icons.list),
label: 'Pesanan',
+132
View File
@@ -0,0 +1,132 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import '../../../common/data/service_data.dart';
import '../../../common/theme/theme.dart';
import '../../../sample/product_sample_data.dart';
import '../../components/delegate/category_delegate.dart';
import 'widgets/header.dart';
import 'widgets/product_section.dart';
@RoutePage()
class MenuPage extends StatefulWidget {
final Service service;
const MenuPage({super.key, required this.service});
@override
State<MenuPage> createState() => _MenuPageState();
}
class _MenuPageState extends State<MenuPage> {
final ScrollController _scrollController = ScrollController();
final List<GlobalKey> _productSectionKeys = [];
String _selectedCategoryId = "";
@override
void initState() {
super.initState();
_selectedCategoryId = categories.isNotEmpty ? categories.first.id : "";
// Initialize keys for each category
for (int i = 0; i < categories.length; i++) {
_productSectionKeys.add(GlobalKey());
}
}
void _scrollToCategory(String categoryId) {
setState(() {
_selectedCategoryId = categoryId;
});
final categoryIndex = categories.indexWhere((cat) => cat.id == categoryId);
if (categoryIndex >= 0 && categoryIndex < _productSectionKeys.length) {
final key = _productSectionKeys[categoryIndex];
final context = key.currentContext;
if (context != null) {
Future.delayed(Duration(milliseconds: 100), () {
if (mounted) {
Scrollable.ensureVisible(
context,
duration: Duration(milliseconds: 500),
curve: Curves.easeInOut,
alignment: 0.1,
);
}
});
}
}
}
List<Product> _getProductsByCategory(String categoryId) {
return products
.where((product) => product.categoryId == categoryId)
.toList();
}
@override
void dispose() {
_scrollController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppColor.background,
body: CustomScrollView(
controller: _scrollController,
slivers: [
// App Bar with merchant info
SliverAppBar(
expandedHeight: 200,
pinned: true,
backgroundColor: AppColor.primary,
leading: IconButton(
icon: Icon(Icons.arrow_back, color: AppColor.textWhite),
onPressed: () => Navigator.of(context).pop(),
),
actions: [
IconButton(
icon: Icon(Icons.share, color: AppColor.textWhite),
onPressed: () {},
),
IconButton(
icon: Icon(Icons.favorite_border, color: AppColor.textWhite),
onPressed: () {},
),
],
flexibleSpace: FlexibleSpaceBar(
background: MenuHeader(service: widget.service),
),
),
// Categories (will be pinned)
SliverPersistentHeader(
pinned: true,
delegate: SliverCategoryDelegate(
categories: categories,
selectedCategoryId: _selectedCategoryId,
onCategoryTap: _scrollToCategory,
),
),
// Product sections by category
...categories.map((category) {
final categoryProducts = _getProductsByCategory(category.id);
final categoryIndex = categories.indexOf(category);
return SliverToBoxAdapter(
key: _productSectionKeys[categoryIndex],
child: MenuProductSection(
category: category,
categoryProducts: categoryProducts,
),
);
}),
],
),
);
}
}
@@ -0,0 +1,166 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import '../../../../../common/extension/extension.dart';
import '../../../../../common/theme/theme.dart';
import '../../../../../sample/product_sample_data.dart';
import '../../../../components/button/button.dart';
import '../../../../components/card/variant_card.dart';
import '../../../../components/image/image.dart';
import '../../../../router/app_router.gr.dart';
@RoutePage()
class MenuDetailPage extends StatefulWidget {
final Product product;
const MenuDetailPage({super.key, required this.product});
@override
State<MenuDetailPage> createState() => _MenuDetailPageState();
}
class _MenuDetailPageState extends State<MenuDetailPage> {
final ScrollController _scrollController = ScrollController();
double _titleOpacity = 0.0;
@override
void initState() {
super.initState();
_scrollController.addListener(_onScroll);
}
void _onScroll() {
// Hitung opacity berdasarkan scroll offset
// Mulai muncul dari offset 150, full opacity di offset 220
double offset = _scrollController.offset;
double newOpacity = ((offset - 150) / 70).clamp(0.0, 1.0);
if (newOpacity != _titleOpacity) {
setState(() => _titleOpacity = newOpacity);
}
}
@override
void dispose() {
_scrollController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
bottomNavigationBar: Container(
padding: EdgeInsets.all(AppValue.padding).copyWith(bottom: 24),
decoration: BoxDecoration(
color: AppColor.white,
boxShadow: [
BoxShadow(
color: AppColor.black.withOpacity(0.1),
offset: Offset(2, 0),
blurRadius: 10,
),
],
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Expanded(flex: 1, child: QtyButton()),
SizedBox(width: 16),
Expanded(
flex: 2,
child: AppElevatedButton(
onPressed: () => context.router.push(CheckoutRoute()),
title: '+ Keranjang ${"27000".currencyFormatRp}',
),
),
],
),
),
body: CustomScrollView(
controller: _scrollController,
slivers: [
SliverAppBar(
expandedHeight: 240,
pinned: true,
backgroundColor: Colors.white,
title: AnimatedOpacity(
opacity: _titleOpacity,
duration: Duration(milliseconds: 200),
child: Text(
widget.product.name,
style: TextStyle(
color: Colors.black,
fontWeight: FontWeight.bold,
),
),
),
flexibleSpace: FlexibleSpaceBar(
background: Stack(
children: [
ImagePlaceholder(width: double.infinity, height: 240),
],
),
),
),
SliverToBoxAdapter(
child: Container(
padding: EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
widget.product.name,
style: AppStyle.xl.copyWith(fontWeight: FontWeight.bold),
),
SizedBox(height: 4),
Text(
widget.product.description,
style: AppStyle.md.copyWith(fontWeight: FontWeight.w500),
),
SizedBox(height: 8),
Align(
alignment: Alignment.centerRight,
child: Text(
widget.product.price.currencyFormatRp,
style: AppStyle.xxl.copyWith(
color: AppColor.primary,
fontWeight: FontWeight.w700,
),
),
),
],
),
),
),
SliverToBoxAdapter(
child: Container(
width: double.infinity,
height: 6,
decoration: BoxDecoration(color: AppColor.borderLight),
),
),
SliverToBoxAdapter(
child: Container(
padding: EdgeInsets.all(16),
decoration: BoxDecoration(),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Pilih Varian',
style: AppStyle.lg.copyWith(fontWeight: FontWeight.bold),
),
SizedBox(height: 12),
VariantCard(name: 'Small'),
SizedBox(height: 8),
VariantCard(name: 'Normal', isSelected: true),
SizedBox(height: 8),
VariantCard(name: 'Large'),
],
),
),
),
],
),
);
}
}
@@ -0,0 +1,142 @@
import 'package:flutter/material.dart';
import '../../../../common/constant/app_constant.dart';
import '../../../../common/data/service_data.dart';
import '../../../../common/theme/theme.dart';
class MenuHeader extends StatelessWidget {
final Service service;
const MenuHeader({super.key, required this.service});
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [AppColor.primary, AppColor.primary.withOpacity(0.8)],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
),
child: Stack(
children: [
// Background decoration
Positioned(
right: 20,
top: 60,
child: Opacity(
opacity: 0.1,
child: Icon(Icons.store, size: 100, color: AppColor.textWhite),
),
),
// Content
Positioned(
left: 20,
right: 20,
bottom: 20,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Service
Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
service.name,
style: AppStyle.lg.copyWith(
color: AppColor.white,
fontWeight: FontWeight.bold,
),
),
Text(
service.description,
style: AppStyle.md.copyWith(
color: AppColor.white,
fontWeight: FontWeight.w500,
),
),
],
),
),
SizedBox(width: 12),
Image.asset(service.imagePath, width: 60, height: 60),
],
),
SizedBox(height: 16),
// Merchant
Row(
children: [
Container(
width: 60,
height: 60,
decoration: BoxDecoration(
color: AppColor.surface,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: AppColor.black.withOpacity(0.2),
blurRadius: 8,
offset: Offset(0, 4),
),
],
),
child: Center(
child: ClipRRect(
borderRadius: BorderRadius.circular(12),
child: Image.asset(merchant.imageUrl),
),
),
),
SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
merchant.name,
style: AppStyle.h6.copyWith(
color: AppColor.textWhite,
fontWeight: FontWeight.bold,
),
),
Text(
merchant.category,
style: AppStyle.md.copyWith(
color: AppColor.textWhite.withOpacity(0.9),
),
),
Row(children: [SizedBox(width: 12)]),
],
),
),
Container(
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: merchant.isOpen
? AppColor.success
: AppColor.error,
borderRadius: BorderRadius.circular(6),
),
child: Text(
merchant.isOpen ? "BUKA" : "TUTUP",
style: AppStyle.xs.copyWith(
color: AppColor.textWhite,
fontWeight: FontWeight.bold,
),
),
),
],
),
],
),
),
],
),
);
}
}
@@ -0,0 +1,77 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import '../../../../common/theme/theme.dart';
import '../../../../sample/product_sample_data.dart';
import '../../../components/card/product_card.dart';
import '../../../components/card/product_empty_card.dart';
import '../../../router/app_router.gr.dart';
class MenuProductSection extends StatelessWidget {
final ProductCategory category;
final List<Product> categoryProducts;
const MenuProductSection({
super.key,
required this.category,
required this.categoryProducts,
});
@override
Widget build(BuildContext context) {
return Container(
margin: EdgeInsets.only(bottom: 24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Section header
Padding(
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 16),
child: Row(
children: [
Text(category.icon, style: AppStyle.h5),
SizedBox(width: 8),
Text(
category.name,
style: AppStyle.lg.copyWith(
fontWeight: FontWeight.bold,
color: AppColor.textPrimary,
),
),
SizedBox(width: 8),
Text(
"(${categoryProducts.length})",
style: AppStyle.md.copyWith(color: AppColor.textSecondary),
),
],
),
),
// Products grid
if (categoryProducts.isEmpty)
ProductEmptyCard()
else
GridView.builder(
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
padding: EdgeInsets.symmetric(horizontal: 16),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 12,
mainAxisSpacing: 12,
childAspectRatio: 0.75,
),
itemCount: categoryProducts.length,
itemBuilder: (context, index) {
return ProductCard(
product: categoryProducts[index],
onTap: () => context.router.push(
MenuDetailRoute(product: categoryProducts[index]),
),
);
},
),
],
),
);
}
}
@@ -1,47 +1,9 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import '../../../../../common/theme/theme.dart';
import '../../../../../sample/product_sample_data.dart';
import '../../../../../sample/sample_data.dart';
// Models
class ProductCategory {
final String id;
final String name;
final String icon;
final int productCount;
ProductCategory({
required this.id,
required this.name,
required this.icon,
required this.productCount,
});
}
class Product {
final String id;
final String name;
final String description;
final int price;
final String categoryId;
final String imageUrl;
final bool isAvailable;
final double rating;
final int soldCount;
Product({
required this.id,
required this.name,
required this.description,
required this.price,
required this.categoryId,
required this.imageUrl,
required this.isAvailable,
required this.rating,
required this.soldCount,
});
}
@RoutePage()
class MerchantDetailPage extends StatefulWidget {
final MerchantModel merchant;
@@ -58,89 +20,6 @@ class _MerchantDetailPageState extends State<MerchantDetailPage> {
String _selectedCategoryId = "";
// Sample data
final List<ProductCategory> categories = [
ProductCategory(id: "1", name: "Makanan", icon: "🍽️", productCount: 8),
ProductCategory(id: "2", name: "Minuman", icon: "🥤", productCount: 6),
ProductCategory(id: "3", name: "Snack", icon: "🍿", productCount: 5),
ProductCategory(id: "4", name: "Es Krim", icon: "🍦", productCount: 4),
ProductCategory(id: "5", name: "Paket", icon: "📦", productCount: 3),
];
final List<Product> products = [
// Makanan
Product(
id: "1",
name: "Nasi Gudeg",
description: "Gudeg khas Yogyakarta dengan ayam dan telur",
price: 25000,
categoryId: "1",
imageUrl: "https://via.placeholder.com/150",
isAvailable: true,
rating: 4.5,
soldCount: 50,
),
Product(
id: "2",
name: "Soto Ayam",
description: "Soto ayam kuning dengan nasi dan kerupuk",
price: 18000,
categoryId: "1",
imageUrl: "https://via.placeholder.com/150",
isAvailable: true,
rating: 4.3,
soldCount: 75,
),
Product(
id: "3",
name: "Gado-gado",
description: "Gado-gado segar dengan bumbu kacang",
price: 15000,
categoryId: "1",
imageUrl: "https://via.placeholder.com/150",
isAvailable: false,
rating: 4.2,
soldCount: 30,
),
// Minuman
Product(
id: "4",
name: "Es Teh Manis",
description: "Es teh manis segar",
price: 5000,
categoryId: "2",
imageUrl: "https://via.placeholder.com/150",
isAvailable: true,
rating: 4.0,
soldCount: 120,
),
Product(
id: "5",
name: "Jus Jeruk",
description: "Jus jeruk segar tanpa gula tambahan",
price: 12000,
categoryId: "2",
imageUrl: "https://via.placeholder.com/150",
isAvailable: true,
rating: 4.4,
soldCount: 45,
),
// Snack
Product(
id: "6",
name: "Keripik Pisang",
description: "Keripik pisang renyah dan manis",
price: 8000,
categoryId: "3",
imageUrl: "https://via.placeholder.com/150",
isAvailable: true,
rating: 4.1,
soldCount: 25,
),
];
@override
void initState() {
super.initState();
@@ -20,6 +20,7 @@ class PrizeHistory {
final int value;
final Color color;
final IconData icon;
final String? gamePrizeId; // Added for API integration
PrizeHistory({
required this.prize,
@@ -27,5 +28,73 @@ class PrizeHistory {
required this.value,
required this.color,
required this.icon,
this.gamePrizeId,
});
// CopyWith method for updating properties
PrizeHistory copyWith({
String? prize,
DateTime? dateTime,
int? value,
Color? color,
IconData? icon,
String? gamePrizeId,
}) {
return PrizeHistory(
prize: prize ?? this.prize,
dateTime: dateTime ?? this.dateTime,
value: value ?? this.value,
color: color ?? this.color,
icon: icon ?? this.icon,
gamePrizeId: gamePrizeId ?? this.gamePrizeId,
);
}
// Convert to Map for storage/API calls
Map<String, dynamic> toMap() {
return {
'prize': prize,
'dateTime': dateTime.toIso8601String(),
'value': value,
'gamePrizeId': gamePrizeId,
};
}
// Create from Map for loading from storage/API
factory PrizeHistory.fromMap(
Map<String, dynamic> map, {
required Color color,
required IconData icon,
}) {
return PrizeHistory(
prize: map['prize'] ?? '',
dateTime: DateTime.tryParse(map['dateTime'] ?? '') ?? DateTime.now(),
value: map['value'] ?? 0,
color: color,
icon: icon,
gamePrizeId: map['gamePrizeId'],
);
}
// Format date for display
String get formattedDate {
return '${dateTime.day.toString().padLeft(2, '0')}/${dateTime.month.toString().padLeft(2, '0')}/${dateTime.year} ${dateTime.hour.toString().padLeft(2, '0')}:${dateTime.minute.toString().padLeft(2, '0')}';
}
// Get prize category based on value
String get prizeCategory {
if (value >= 1000000) return 'Jackpot';
if (value >= 100000) return 'Big Prize';
if (value >= 10000) return 'Medium Prize';
if (value >= 1000) return 'Small Prize';
return 'Token/Spin';
}
// Get prize category color
Color get categoryColor {
if (value >= 1000000) return const Color(0xFFFFD700); // Gold
if (value >= 100000) return const Color(0xFFC0C0C0); // Silver
if (value >= 10000) return const Color(0xFFCD7F32); // Bronze
return color; // Default color
}
}
@@ -1,18 +1,50 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:audioplayers/audioplayers.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_spinkit/flutter_spinkit.dart';
import 'dart:math' as math;
import '../../../../application/auth/auth_bloc.dart';
import '../../../../application/game/ferris_wheel_loader/ferris_wheel_loader_bloc.dart';
import '../../../../common/theme/theme.dart';
import '../../../../common/painter/wheel_painter.dart';
import 'data/model.dart';
import '../../../../injection.dart';
import '../../../../domain/game/game.dart';
// Simple models for UI state
class PrizeHistory {
final String prize;
final DateTime dateTime;
final int value;
final Color color;
final IconData icon;
final String? gamePrizeId;
PrizeHistory({
required this.prize,
required this.dateTime,
required this.value,
required this.color,
required this.icon,
this.gamePrizeId,
});
}
@RoutePage()
class FerrisWheelPage extends StatefulWidget {
class FerrisWheelPage extends StatefulWidget implements AutoRouteWrapper {
const FerrisWheelPage({super.key});
@override
State<FerrisWheelPage> createState() => _FerrisWheelPageState();
@override
Widget wrappedRoute(BuildContext context) => BlocProvider(
create: (context) =>
getIt<FerrisWheelLoaderBloc>()
..add(const FerrisWheelLoaderEvent.fetched()),
child: this,
);
}
class _FerrisWheelPageState extends State<FerrisWheelPage>
@@ -45,56 +77,7 @@ class _FerrisWheelPageState extends State<FerrisWheelPage>
// Game Data
List<PrizeHistory> prizeHistory = [];
List<WheelSection> wheelSections = [
WheelSection(
color: AppColor.primary,
icon: Icons.visibility,
prize: '1 JT',
value: 1000000,
),
WheelSection(
color: AppColor.info,
icon: Icons.account_balance_wallet,
prize: '10RB',
value: 10000,
),
WheelSection(
color: AppColor.warning,
icon: Icons.card_giftcard,
prize: '50',
value: 50,
),
WheelSection(
color: AppColor.success,
icon: Icons.refresh,
prize: 'SPIN',
value: 1,
),
WheelSection(
color: AppColor.primaryDark,
icon: Icons.visibility,
prize: '30',
value: 30,
),
WheelSection(
color: AppColor.textLight,
icon: Icons.attach_money,
prize: '5RB',
value: 5000,
),
WheelSection(
color: AppColor.secondary,
icon: Icons.redeem,
prize: '20',
value: 20,
),
WheelSection(
color: AppColor.info,
icon: Icons.monetization_on,
prize: '5RB',
value: 5000,
),
];
List<GamePrize> gamePrizes = [];
@override
void initState() {
@@ -116,12 +99,10 @@ class _FerrisWheelPageState extends State<FerrisWheelPage>
void _initializeAudio() async {
try {
await _bgmPlayer.setSource(
AssetSource('audio/carnival/bgm/carnival_main_theme.mp3'),
);
await _bgmPlayer.setSource(AssetSource('audio/carnaval_main_theme.mp3'));
await _bgmPlayer.setReleaseMode(ReleaseMode.loop);
await _bgmPlayer.setVolume(0.5);
if (_isMusicEnabled) _bgmPlayer.resume();
if (_isMusicEnabled) await _bgmPlayer.resume();
} catch (e) {
print('Error initializing audio: $e');
}
@@ -139,22 +120,10 @@ class _FerrisWheelPageState extends State<FerrisWheelPage>
}
}
void _playButtonTap() =>
_playSound('audio/carnival/sfx/button_tap.mp3', volume: 0.3);
void _playTokenSound() =>
_playSound('audio/carnival/sfx/token_sound.mp3', volume: 0.5);
void _playWheelSpin() =>
_playSound('audio/carnival/sfx/wheel_spin.mp3', volume: 0.8);
void _playWinSound(int prizeValue) {
if (prizeValue >= 1000000) {
_playSound('audio/carnival/sfx/win_big.mp3', volume: 0.9);
} else if (prizeValue >= 5000) {
_playSound('audio/carnival/sfx/win_medium.mp3', volume: 0.8);
} else {
_playSound('audio/carnival/sfx/win_small.mp3', volume: 0.7);
}
}
void _playButtonTap() => _playSound('audio/button_tap.mp3', volume: 0.3);
void _playTokenSound() => _playSound('audio/token_sound.mp3', volume: 0.5);
void _playWheelSpin() => _playSound('audio/wheel_spin.mp3', volume: 0.8);
void _playWinSound() => _playSound('audio/win_medium.mp3', volume: 0.8);
void _toggleSound() {
setState(() => _isSoundEnabled = !_isSoundEnabled);
@@ -198,8 +167,55 @@ class _FerrisWheelPageState extends State<FerrisWheelPage>
_idleRotationController.repeat();
}
int _selectPrizeWithWeight() {
if (gamePrizes.isEmpty) return 0;
// Jika GamePrize memiliki properti weight, gunakan ini:
// List<int> weights = gamePrizes.map((prize) => prize.weight ?? 1).toList();
// Untuk sementara, gunakan bobot default yang berbeda
// Hadiah langka (index awal) memiliki bobot lebih kecil
List<int> weights = [];
for (int i = 0; i < gamePrizes.length; i++) {
// Bobot menurun: hadiah pertama langka, hadiah terakhir mudah didapat
int weight = gamePrizes.length - i;
weights.add(weight);
}
// Hitung total bobot
int totalWeight = weights.reduce((a, b) => a + b);
// Generate random number
int randomNum = math.Random().nextInt(totalWeight);
// Tentukan hadiah berdasarkan bobot
int currentWeight = 0;
for (int i = 0; i < gamePrizes.length; i++) {
currentWeight += weights[i];
if (randomNum < currentWeight) {
return i;
}
}
// Fallback (seharusnya tidak pernah terjadi)
return gamePrizes.length - 1;
}
Color _getPrizeColor(GamePrize prize, int index) {
final colors = [
AppColor.primary,
AppColor.info,
AppColor.warning,
AppColor.success,
AppColor.primaryDark,
AppColor.secondary,
AppColor.error,
];
return colors[index % colors.length];
}
void _spinWheel() {
if (isSpinning || tokens <= 0) return;
if (isSpinning || tokens <= 0 || gamePrizes.isEmpty) return;
_playWheelSpin();
_playTokenSound();
@@ -211,23 +227,25 @@ class _FerrisWheelPageState extends State<FerrisWheelPage>
});
_idleRotationController.stop();
_idleRotationController.reset();
int targetSection = math.Random().nextInt(8);
double sectionAngle = (2 * math.pi) / 8;
double targetAngle = (targetSection * sectionAngle) + (sectionAngle / 2);
double baseRotations = 4 + math.Random().nextDouble() * 3;
double currentIdleRotation = _idleRotationAnimation?.value ?? 0.0;
double finalRotation =
currentRotation +
currentIdleRotation +
(baseRotations * 2 * math.pi) +
targetAngle;
int selectedPrizeIndex = _selectPrizeWithWeight();
double sectionAngle = (2 * math.pi) / gamePrizes.length;
double currentPos = currentRotation;
_spinAnimation =
Tween<double>(
begin: currentRotation + currentIdleRotation,
end: finalRotation,
).animate(
// TAMBAH offset ke tengah section (bukan garis)
double targetForSelectedSection =
-(selectedPrizeIndex * sectionAngle) - (sectionAngle / 2);
double spins = 6 * 2 * math.pi;
double finalRotation = currentPos + spins + targetForSelectedSection;
while (finalRotation <= currentPos + spins) {
finalRotation += 2 * math.pi;
}
_spinAnimation = Tween<double>(begin: currentPos, end: finalRotation)
.animate(
CurvedAnimation(
parent: _rotationController,
curve: Curves.easeOutCubic,
@@ -236,32 +254,32 @@ class _FerrisWheelPageState extends State<FerrisWheelPage>
_rotationController.reset();
_rotationController.animateTo(1.0).then((_) {
_playWinSound(wheelSections[targetSection].value);
final wonPrize = gamePrizes[selectedPrizeIndex];
_playWinSound();
setState(() {
currentRotation = finalRotation;
isSpinning = false;
resultText =
'Selamat! Anda mendapat ${wheelSections[targetSection].prize}!';
resultText = 'Selamat! Anda mendapat ${wonPrize.name}!';
prizeHistory.insert(
0,
PrizeHistory(
prize: wheelSections[targetSection].prize,
prize: wonPrize.name,
dateTime: DateTime.now(),
value: wheelSections[targetSection].value,
color: wheelSections[targetSection].color,
icon: wheelSections[targetSection].icon,
value: 100,
color: _getPrizeColor(wonPrize, selectedPrizeIndex),
icon: Icons.card_giftcard,
gamePrizeId: wonPrize.id,
),
);
});
_showWinDialog(wheelSections[targetSection]);
_idleRotationController.reset();
_showWinDialog(wonPrize);
_idleRotationController.repeat();
});
}
void _showWinDialog(WheelSection section) {
void _showWinDialog(GamePrize wonPrize) {
showDialog(
context: context,
barrierDismissible: false,
@@ -294,7 +312,11 @@ class _FerrisWheelPageState extends State<FerrisWheelPage>
),
],
),
child: Icon(section.icon, color: section.color, size: 40),
child: Icon(
Icons.card_giftcard,
color: AppColor.primary,
size: 40,
),
),
const SizedBox(height: 16),
Text(
@@ -311,7 +333,7 @@ class _FerrisWheelPageState extends State<FerrisWheelPage>
),
const SizedBox(height: 4),
Text(
section.prize,
wonPrize.name,
style: AppStyle.h5.copyWith(
fontWeight: FontWeight.bold,
color: AppColor.textWhite,
@@ -351,6 +373,33 @@ class _FerrisWheelPageState extends State<FerrisWheelPage>
@override
Widget build(BuildContext context) {
return BlocBuilder<FerrisWheelLoaderBloc, FerrisWheelLoaderState>(
builder: (context, state) {
if (state.isFetching) {
return Scaffold(
body: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: AppColor.primaryGradient,
),
),
child: Center(
child: SpinKitFadingCircle(color: AppColor.textWhite, size: 36),
),
),
);
}
if (gamePrizes.isEmpty && state.ferrisWheel.prizes.isNotEmpty) {
WidgetsBinding.instance.addPostFrameCallback((_) {
setState(() {
gamePrizes = state.ferrisWheel.prizes;
});
});
}
return Scaffold(
body: Container(
decoration: BoxDecoration(
@@ -363,8 +412,27 @@ class _FerrisWheelPageState extends State<FerrisWheelPage>
child: SafeArea(
child: Column(
children: [
// Header
Container(
_buildHeader(),
_buildTabSelector(),
const SizedBox(height: 20),
Expanded(
child: currentTabIndex == 0
? _buildMainContent()
: currentTabIndex == 1
? _buildPrizeListContent()
: _buildHistoryContent(),
),
],
),
),
),
);
},
);
}
Widget _buildHeader() {
return Container(
padding: const EdgeInsets.all(16),
child: Row(
children: [
@@ -373,11 +441,7 @@ class _FerrisWheelPageState extends State<FerrisWheelPage>
_playButtonTap();
context.router.back();
},
icon: Icon(
Icons.close,
color: AppColor.textWhite,
size: 28,
),
icon: Icon(Icons.close, color: AppColor.textWhite, size: 28),
),
Expanded(
child: Text(
@@ -405,10 +469,11 @@ class _FerrisWheelPageState extends State<FerrisWheelPage>
),
],
),
),
);
}
// Tab Selector
Container(
Widget _buildTabSelector() {
return Container(
margin: const EdgeInsets.symmetric(horizontal: 20),
decoration: BoxDecoration(
color: AppColor.white.withOpacity(0.2),
@@ -446,30 +511,32 @@ class _FerrisWheelPageState extends State<FerrisWheelPage>
),
],
),
),
const SizedBox(height: 20),
// Content Area
Expanded(
child: currentTabIndex == 0
? _buildMainContent()
: currentTabIndex == 1
? _buildPrizeListContent()
: _buildHistoryContent(),
),
],
),
),
),
);
}
Widget _buildMainContent() {
return Column(
children: [
// User Info Card
Container(
_buildUserInfoCard(),
Text(
resultText,
style: AppStyle.md.copyWith(color: AppColor.textWhite),
),
const SizedBox(height: 20),
Expanded(
child: Center(
child: gamePrizes.isEmpty
? SpinKitFadingCircle(color: AppColor.textWhite, size: 36)
: _buildWheelSection(),
),
),
_buildBottomBanner(),
],
);
}
Widget _buildUserInfoCard() {
return Container(
margin: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
@@ -486,7 +553,9 @@ class _FerrisWheelPageState extends State<FerrisWheelPage>
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
BlocBuilder<AuthBloc, AuthState>(
builder: (context, auth) {
return Row(
children: [
Container(
width: 40,
@@ -510,14 +579,14 @@ class _FerrisWheelPageState extends State<FerrisWheelPage>
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'GPY268e42',
auth.user.name,
style: AppStyle.lg.copyWith(
fontWeight: FontWeight.bold,
color: AppColor.textPrimary,
),
),
Text(
'Warrior',
auth.user.phoneNumber,
style: AppStyle.sm.copyWith(
color: AppColor.textSecondary,
),
@@ -525,6 +594,8 @@ class _FerrisWheelPageState extends State<FerrisWheelPage>
],
),
],
);
},
),
Row(
children: [
@@ -535,11 +606,7 @@ class _FerrisWheelPageState extends State<FerrisWheelPage>
color: AppColor.warning,
shape: BoxShape.circle,
),
child: Icon(
Icons.circle,
size: 12,
color: AppColor.textWhite,
),
child: Icon(Icons.circle, size: 12, color: AppColor.textWhite),
),
const SizedBox(width: 8),
Text(
@@ -553,18 +620,11 @@ class _FerrisWheelPageState extends State<FerrisWheelPage>
),
],
),
),
);
}
Text(
resultText,
style: AppStyle.md.copyWith(color: AppColor.textWhite),
),
const SizedBox(height: 20),
// Wheel Section
Expanded(
child: Center(
child: Stack(
Widget _buildWheelSection() {
return Stack(
alignment: Alignment.center,
children: [
// Glow Effect
@@ -587,32 +647,50 @@ class _FerrisWheelPageState extends State<FerrisWheelPage>
),
),
),
// Spinning Wheel
AnimatedBuilder(
animation: isSpinning
? _rotationController
: _idleRotationController,
animation: isSpinning ? _rotationController : _idleRotationController,
builder: (context, child) {
double rotationAngle = isSpinning
? (_spinAnimation?.value ?? currentRotation)
: (currentRotation +
(_idleRotationAnimation?.value ?? 0.0));
: (currentRotation + (_idleRotationAnimation?.value ?? 0.0));
return Transform.rotate(
angle: rotationAngle,
child: CustomPaint(
size: const Size(320, 320),
painter: WheelPainter(sections: wheelSections),
painter: WheelPainter(
gamePrizes: gamePrizes,
getPrizeColor: _getPrizeColor,
),
),
);
},
),
// Spin Button
AnimatedBuilder(
animation:
_pulseAnimation ?? const AlwaysStoppedAnimation(1.0),
_buildSpinButton(),
// Pointer
Positioned(
top: 30,
child: Container(
width: 0,
height: 0,
decoration: BoxDecoration(
border: Border(
left: BorderSide(width: 15, color: Colors.transparent),
right: BorderSide(width: 15, color: Colors.transparent),
bottom: BorderSide(width: 30, color: AppColor.error),
),
),
),
),
],
);
}
Widget _buildSpinButton() {
return AnimatedBuilder(
animation: _pulseAnimation ?? const AlwaysStoppedAnimation(1.0),
builder: (context, child) => Transform.scale(
scale: _pulseAnimation?.value ?? 1.0,
child: Container(
@@ -655,36 +733,15 @@ class _FerrisWheelPageState extends State<FerrisWheelPage>
),
),
),
),
);
}
// Pointer
Positioned(
top: 30,
child: Container(
width: 0,
height: 0,
decoration: BoxDecoration(
border: Border(
left: BorderSide(width: 15, color: Colors.transparent),
right: BorderSide(width: 15, color: Colors.transparent),
bottom: BorderSide(width: 30, color: AppColor.error),
),
),
),
),
],
),
),
),
// Bottom Banner
Container(
Widget _buildBottomBanner() {
return Container(
margin: const EdgeInsets.all(20),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [AppColor.warning, AppColor.warning],
),
gradient: LinearGradient(colors: [AppColor.warning, AppColor.warning]),
borderRadius: BorderRadius.circular(15),
),
child: Text(
@@ -695,8 +752,6 @@ class _FerrisWheelPageState extends State<FerrisWheelPage>
fontWeight: FontWeight.bold,
),
),
),
],
);
}
@@ -729,10 +784,17 @@ class _FerrisWheelPageState extends State<FerrisWheelPage>
),
const SizedBox(height: 20),
Expanded(
child: ListView.builder(
itemCount: wheelSections.length,
child: gamePrizes.isEmpty
? Center(
child: SpinKitFadingCircle(
color: AppColor.white.withOpacity(0.7),
size: 36,
),
)
: ListView.builder(
itemCount: gamePrizes.length,
itemBuilder: (context, index) {
final section = wheelSections[index];
final prize = gamePrizes[index];
return Container(
margin: const EdgeInsets.only(bottom: 12),
padding: const EdgeInsets.all(16),
@@ -753,11 +815,11 @@ class _FerrisWheelPageState extends State<FerrisWheelPage>
width: 50,
height: 50,
decoration: BoxDecoration(
color: section.color,
color: _getPrizeColor(prize, index),
borderRadius: BorderRadius.circular(25),
),
child: Icon(
section.icon,
Icons.card_giftcard,
color: AppColor.white,
size: 24,
),
@@ -768,18 +830,24 @@ class _FerrisWheelPageState extends State<FerrisWheelPage>
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
section.prize,
prize.name,
style: AppStyle.lg.copyWith(
fontWeight: FontWeight.bold,
color: AppColor.textPrimary,
),
),
Text(
'Nilai: ${section.value}',
'ID: ${prize.id}',
style: AppStyle.sm.copyWith(
color: AppColor.textSecondary,
),
),
Text(
'Game ID: ${prize.gameId}',
style: AppStyle.xs.copyWith(
color: AppColor.textSecondary,
),
),
],
),
),
@@ -789,13 +857,16 @@ class _FerrisWheelPageState extends State<FerrisWheelPage>
vertical: 6,
),
decoration: BoxDecoration(
color: section.color.withOpacity(0.1),
color: _getPrizeColor(
prize,
index,
).withOpacity(0.1),
borderRadius: BorderRadius.circular(15),
),
child: Text(
'Hadiah',
style: AppStyle.sm.copyWith(
color: section.color,
style: AppStyle.xs.copyWith(
color: _getPrizeColor(prize, index),
fontWeight: FontWeight.bold,
),
),
@@ -918,6 +989,14 @@ class _FerrisWheelPageState extends State<FerrisWheelPage>
color: AppColor.textSecondary,
),
),
if (history.gamePrizeId != null)
Text(
'ID: ${history.gamePrizeId}',
style: AppStyle.xs.copyWith(
color: AppColor.textSecondary
.withOpacity(0.7),
),
),
],
),
),
@@ -0,0 +1,880 @@
import 'dart:math';
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import '../../../../common/theme/theme.dart';
@RoutePage()
class MisteryBoxPage extends StatefulWidget {
const MisteryBoxPage({super.key});
@override
State<MisteryBoxPage> createState() => _MisteryBoxPageState();
}
class _MisteryBoxPageState extends State<MisteryBoxPage>
with TickerProviderStateMixin {
int coins = 100;
int totalWins = 0;
List<Prize> prizes = [
Prize(
name: 'Voucher Rp 100K',
type: 'Voucher',
value: 100000,
rarity: 'Legendary',
chance: 5,
icon: '🎁',
),
Prize(
name: 'Cashback 50%',
type: 'Cashback',
value: 50,
rarity: 'Epic',
chance: 10,
icon: '💰',
),
Prize(
name: '500 Poin Reward',
type: 'Poin',
value: 500,
rarity: 'Rare',
chance: 15,
icon: '🏆',
),
Prize(
name: 'Voucher Rp 25K',
type: 'Voucher',
value: 25000,
rarity: 'Uncommon',
chance: 25,
icon: '🎫',
),
Prize(
name: '100 Poin Reward',
type: 'Poin',
value: 100,
rarity: 'Common',
chance: 30,
icon: '',
),
Prize(
name: 'Diskon 10%',
type: 'Diskon',
value: 10,
rarity: 'Common',
chance: 15,
icon: '🍕',
),
];
bool isOpening = false;
late AnimationController _shakeController;
late AnimationController _rotateController;
late Animation<double> _shakeAnimation;
late Animation<double> _rotateAnimation;
@override
void initState() {
super.initState();
_shakeController = AnimationController(
duration: const Duration(milliseconds: 600),
vsync: this,
);
_rotateController = AnimationController(
duration: const Duration(milliseconds: 600),
vsync: this,
);
_shakeAnimation = Tween<double>(begin: 0, end: 15).animate(
CurvedAnimation(parent: _shakeController, curve: Curves.elasticIn),
);
_rotateAnimation = Tween<double>(begin: 0, end: 2 * pi).animate(
CurvedAnimation(parent: _rotateController, curve: Curves.easeInOut),
);
}
@override
void dispose() {
_shakeController.dispose();
_rotateController.dispose();
super.dispose();
}
void openBox() async {
if (coins < 10 || isOpening) return;
setState(() {
isOpening = true;
coins -= 10;
});
// Animasi shake dan rotate
_shakeController.repeat(reverse: true);
_rotateController.forward();
await Future.delayed(const Duration(milliseconds: 1200));
_shakeController.stop();
_rotateController.reset();
// Generate prize
Prize prize = _generatePrize();
setState(() {
totalWins++;
isOpening = false;
});
// Show dialog
if (mounted) {
_showPrizeDialog(prize);
}
}
Prize _generatePrize() {
int random = Random().nextInt(100);
int cumulativeChance = 0;
for (var prize in prizes) {
cumulativeChance += prize.chance;
if (random < cumulativeChance) {
return prize;
}
}
return prizes.last;
}
void _showPrizeDialog(Prize prize) {
showDialog(
context: context,
barrierDismissible: false,
builder: (context) => PrizeDialog(prize: prize),
);
}
Color _getRarityColor(String rarity) {
switch (rarity) {
case 'Legendary':
return const Color(0xFFFFD700);
case 'Epic':
return const Color(0xFFB429F9);
case 'Rare':
return const Color(0xFF3B82F6);
case 'Uncommon':
return const Color(0xFF10B981);
default:
return const Color(0xFF9CA3AF);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
AppColor.primary,
AppColor.primaryDark,
const Color(0xFF4A0000),
],
),
),
child: SafeArea(
child: Column(
children: [
_buildHeader(),
Expanded(
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
_buildMysteryBox(),
const SizedBox(height: 40),
_buildOpenButton(),
const SizedBox(height: 24),
_buildPrizeListButton(),
],
),
),
),
],
),
),
),
);
}
Widget _buildHeader() {
return Padding(
padding: const EdgeInsets.all(16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'🎁 Mystery Box',
style: AppStyle.h5.copyWith(
color: AppColor.white,
fontWeight: FontWeight.bold,
),
),
Text(
'Win Amazing Rewards!',
style: AppStyle.xs.copyWith(
color: AppColor.white.withOpacity(0.8),
),
),
],
),
),
Row(
children: [
_buildStatCard('🪙', coins.toString()),
const SizedBox(width: 8),
_buildStatCard('🏆', totalWins.toString()),
],
),
],
),
);
}
Widget _buildStatCard(String icon, String value) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: AppColor.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(16),
border: Border.all(color: AppColor.white.withOpacity(0.3), width: 1.5),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(icon, style: const TextStyle(fontSize: 16)),
const SizedBox(width: 6),
Text(
value,
style: AppStyle.md.copyWith(
color: AppColor.white,
fontWeight: FontWeight.bold,
),
),
],
),
);
}
Widget _buildMysteryBox() {
return AnimatedBuilder(
animation: Listenable.merge([_shakeAnimation, _rotateAnimation]),
builder: (context, child) {
return Transform.translate(
offset: Offset(
sin(_shakeAnimation.value) * 10,
cos(_shakeAnimation.value) * 5,
),
child: Transform.rotate(
angle: _rotateAnimation.value,
child: Container(
width: 200,
height: 200,
decoration: BoxDecoration(
color: AppColor.white,
borderRadius: BorderRadius.circular(24),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.3),
blurRadius: 30,
offset: const Offset(0, 15),
),
BoxShadow(
color: AppColor.warning.withOpacity(0.5),
blurRadius: 50,
spreadRadius: isOpening ? 8 : 0,
),
],
),
child: Stack(
children: [
Positioned(
top: 15,
left: 15,
child: Container(
width: 30,
height: 30,
decoration: BoxDecoration(
color: AppColor.warning.withOpacity(0.2),
shape: BoxShape.circle,
),
),
),
Positioned(
bottom: 20,
right: 20,
child: Container(
width: 25,
height: 25,
decoration: BoxDecoration(
color: AppColor.success.withOpacity(0.2),
shape: BoxShape.circle,
),
),
),
Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text('🎁', style: TextStyle(fontSize: 70)),
const SizedBox(height: 8),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 4,
),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: AppColor.primaryGradient,
),
borderRadius: BorderRadius.circular(16),
),
child: Text(
'Mystery Box',
style: AppStyle.sm.copyWith(
color: AppColor.white,
fontWeight: FontWeight.bold,
),
),
),
],
),
),
if (isOpening) ...[
const Positioned(
top: 25,
right: 25,
child: Text('', style: TextStyle(fontSize: 24)),
),
const Positioned(
bottom: 30,
left: 25,
child: Text('', style: TextStyle(fontSize: 24)),
),
const Positioned(
top: 50,
left: 30,
child: Text('', style: TextStyle(fontSize: 20)),
),
],
],
),
),
),
);
},
);
}
Widget _buildOpenButton() {
bool canOpen = coins >= 10 && !isOpening;
return GestureDetector(
onTap: canOpen ? openBox : null,
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
padding: const EdgeInsets.symmetric(horizontal: 40, vertical: 16),
decoration: BoxDecoration(
color: canOpen ? AppColor.white : AppColor.white.withOpacity(0.3),
borderRadius: BorderRadius.circular(25),
boxShadow: canOpen
? [
BoxShadow(
color: AppColor.white.withOpacity(0.3),
blurRadius: 15,
offset: const Offset(0, 6),
),
]
: [],
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
isOpening ? Icons.refresh : Icons.card_giftcard,
color: canOpen ? AppColor.primary : AppColor.textLight,
size: 24,
),
const SizedBox(width: 10),
Text(
isOpening ? 'Membuka...' : 'Buka Box',
style: AppStyle.lg.copyWith(
color: canOpen ? AppColor.primary : AppColor.textLight,
fontWeight: FontWeight.bold,
),
),
const SizedBox(width: 8),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
decoration: BoxDecoration(
color: canOpen
? AppColor.warning.withOpacity(0.2)
: AppColor.textLight.withOpacity(0.2),
borderRadius: BorderRadius.circular(10),
),
child: Text(
'10 🪙',
style: AppStyle.xs.copyWith(
color: canOpen ? AppColor.warning : AppColor.textLight,
fontWeight: FontWeight.bold,
),
),
),
],
),
),
);
}
Widget _buildPrizeListButton() {
return GestureDetector(
onTap: () {
showModalBottomSheet(
context: context,
backgroundColor: Colors.transparent,
isScrollControlled: true,
builder: (context) => _buildPrizeListModal(),
);
},
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 12),
decoration: BoxDecoration(
color: AppColor.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(20),
border: Border.all(color: AppColor.white.withOpacity(0.3), width: 2),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.emoji_events, color: AppColor.white, size: 20),
const SizedBox(width: 8),
Text(
'Lihat Hadiah Tersedia',
style: AppStyle.sm.copyWith(
color: AppColor.white,
fontWeight: FontWeight.bold,
),
),
const SizedBox(width: 4),
const Icon(
Icons.arrow_forward_ios,
color: AppColor.white,
size: 14,
),
],
),
),
);
}
Widget _buildPrizeListModal() {
return Container(
height: MediaQuery.of(context).size.height * 0.7,
decoration: const BoxDecoration(
color: AppColor.white,
borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
),
child: Column(
children: [
// Handle bar
Container(
margin: const EdgeInsets.only(top: 12),
width: 40,
height: 4,
decoration: BoxDecoration(
color: AppColor.textLight,
borderRadius: BorderRadius.circular(2),
),
),
// Header
Padding(
padding: const EdgeInsets.all(20),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Hadiah Tersedia',
style: AppStyle.h5.copyWith(
fontWeight: FontWeight.bold,
color: AppColor.textPrimary,
),
),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 4,
),
decoration: BoxDecoration(
color: AppColor.primary.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: Text(
'${prizes.length} Items',
style: AppStyle.xs.copyWith(
color: AppColor.primary,
fontWeight: FontWeight.bold,
),
),
),
],
),
),
// Prize Grid
Expanded(
child: GridView.builder(
padding: const EdgeInsets.symmetric(horizontal: 20),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
childAspectRatio: 0.70,
crossAxisSpacing: 12,
mainAxisSpacing: 12,
),
itemCount: prizes.length,
itemBuilder: (context, index) {
final prize = prizes[index];
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
_getRarityColor(prize.rarity).withOpacity(0.1),
_getRarityColor(prize.rarity).withOpacity(0.05),
],
),
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: _getRarityColor(prize.rarity).withOpacity(0.4),
width: 2,
),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(prize.icon, style: const TextStyle(fontSize: 40)),
const SizedBox(height: 12),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 4,
),
decoration: BoxDecoration(
color: _getRarityColor(prize.rarity),
borderRadius: BorderRadius.circular(12),
),
child: Text(
prize.rarity,
style: AppStyle.xs.copyWith(
color: AppColor.white,
fontWeight: FontWeight.bold,
fontSize: 9,
),
),
),
const SizedBox(height: 8),
Text(
prize.name,
style: AppStyle.sm.copyWith(
fontWeight: FontWeight.bold,
color: AppColor.textPrimary,
),
textAlign: TextAlign.center,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 4),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 3,
),
decoration: BoxDecoration(
color: AppColor.primary.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Text(
prize.type,
style: AppStyle.xs.copyWith(
color: AppColor.primary,
fontWeight: FontWeight.w600,
fontSize: 10,
),
),
),
const SizedBox(height: 8),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 3,
),
decoration: BoxDecoration(
color: _getRarityColor(
prize.rarity,
).withOpacity(0.2),
borderRadius: BorderRadius.circular(8),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.percent,
size: 10,
color: _getRarityColor(prize.rarity),
),
const SizedBox(width: 2),
Text(
'${prize.chance}%',
style: AppStyle.xs.copyWith(
color: _getRarityColor(prize.rarity),
fontWeight: FontWeight.bold,
fontSize: 10,
),
),
],
),
),
],
),
],
),
);
},
),
),
const SizedBox(height: 20),
],
),
);
}
}
class PrizeDialog extends StatefulWidget {
final Prize prize;
const PrizeDialog({Key? key, required this.prize}) : super(key: key);
@override
State<PrizeDialog> createState() => _PrizeDialogState();
}
class _PrizeDialogState extends State<PrizeDialog>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _scaleAnimation;
late Animation<double> _fadeAnimation;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(milliseconds: 500),
vsync: this,
);
_scaleAnimation = Tween<double>(
begin: 0.5,
end: 1.0,
).animate(CurvedAnimation(parent: _controller, curve: Curves.elasticOut));
_fadeAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(parent: _controller, curve: Curves.easeIn));
_controller.forward();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
Color _getRarityColor(String rarity) {
switch (rarity) {
case 'Legendary':
return const Color(0xFFFFD700);
case 'Epic':
return const Color(0xFFB429F9);
case 'Rare':
return const Color(0xFF3B82F6);
case 'Uncommon':
return const Color(0xFF10B981);
default:
return const Color(0xFF9CA3AF);
}
}
@override
Widget build(BuildContext context) {
return FadeTransition(
opacity: _fadeAnimation,
child: Dialog(
backgroundColor: Colors.transparent,
child: ScaleTransition(
scale: _scaleAnimation,
child: Container(
constraints: const BoxConstraints(maxWidth: 320),
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
color: AppColor.white,
borderRadius: BorderRadius.circular(24),
border: Border.all(
color: _getRarityColor(widget.prize.rarity),
width: 4,
),
boxShadow: [
BoxShadow(
color: _getRarityColor(widget.prize.rarity).withOpacity(0.5),
blurRadius: 30,
offset: const Offset(0, 10),
spreadRadius: 2,
),
],
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text('🎉', style: const TextStyle(fontSize: 50)),
const SizedBox(height: 12),
Text(
'Selamat!',
style: AppStyle.h4.copyWith(
fontWeight: FontWeight.bold,
color: AppColor.textPrimary,
),
),
const SizedBox(height: 8),
Text(
'Kamu mendapatkan',
style: AppStyle.sm.copyWith(color: AppColor.textSecondary),
),
const SizedBox(height: 16),
Text(widget.prize.icon, style: const TextStyle(fontSize: 60)),
const SizedBox(height: 12),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 14,
vertical: 6,
),
decoration: BoxDecoration(
color: _getRarityColor(widget.prize.rarity),
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: _getRarityColor(
widget.prize.rarity,
).withOpacity(0.4),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: Text(
widget.prize.rarity.toUpperCase(),
style: AppStyle.xs.copyWith(
color: AppColor.white,
fontWeight: FontWeight.bold,
letterSpacing: 1.2,
),
),
),
const SizedBox(height: 16),
Text(
widget.prize.name,
style: AppStyle.h5.copyWith(
fontWeight: FontWeight.bold,
color: AppColor.textPrimary,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 4,
),
decoration: BoxDecoration(
color: AppColor.primary.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: Text(
widget.prize.type,
style: AppStyle.xs.copyWith(
color: AppColor.primary,
fontWeight: FontWeight.w600,
),
),
),
const SizedBox(height: 24),
GestureDetector(
onTap: () => Navigator.pop(context),
child: Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(vertical: 14),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: AppColor.primaryGradient,
),
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: AppColor.primary.withOpacity(0.4),
blurRadius: 12,
offset: const Offset(0, 4),
),
],
),
child: Text(
'Tutup',
style: AppStyle.md.copyWith(
color: AppColor.white,
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center,
),
),
),
],
),
),
),
),
);
}
}
class Prize {
final String name;
final String type;
final int value;
final String rarity;
final int chance;
final String icon;
Prize({
required this.name,
required this.type,
required this.value,
required this.rarity,
required this.chance,
required this.icon,
});
}
@@ -0,0 +1,541 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'dart:math' as math;
import '../../../common/constant/app_constant.dart';
import '../../../common/theme/theme.dart';
@RoutePage()
class PointPage extends StatefulWidget {
const PointPage({super.key});
@override
State<PointPage> createState() => _PointPageState();
}
class _PointPageState extends State<PointPage> {
int _currentPage = 0;
final PageController _pageController = PageController(viewportFraction: 1.0);
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
body: SingleChildScrollView(
child: Column(
children: [
// Pink Header with overlapping elements
Stack(
clipBehavior: Clip.none,
children: [
// Pink Background
Container(
height: 320,
width: double.infinity,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
AppColor.primary.withOpacity(0.8),
AppColor.primary,
],
),
),
child: SafeArea(
child: Padding(
padding: const EdgeInsets.all(24.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Back Button
Container(
width: 56,
height: 56,
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.3),
shape: BoxShape.circle,
),
child: IconButton(
icon: const Icon(
Icons.arrow_back,
color: Colors.white,
size: 28,
),
onPressed: () => Navigator.pop(context),
),
),
const SizedBox(height: 32),
// Title
Text(
'Kelola ${AppConstant.poinName} kamu!',
style: AppStyle.h3.copyWith(
color: AppColor.white,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
// Subtitle
Text(
'Bisa kumpulin dan tukar dari sini.',
style: AppStyle.md.copyWith(
color: AppColor.white.withOpacity(0.9),
),
),
],
),
),
),
),
// Point Card - Overlapping
Positioned(
top: 280,
left: 24,
right: 24,
child: Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: AppColor.white,
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 56,
height: 56,
decoration: BoxDecoration(
color: AppColor.primary,
shape: BoxShape.circle,
),
child: const Icon(
Icons.card_giftcard,
color: Colors.white,
size: 32,
),
),
const SizedBox(width: 16),
Text(
'50 ${AppConstant.poinName}',
style: AppStyle.h4.copyWith(
color: AppColor.primary,
fontWeight: FontWeight.bold,
),
),
],
),
),
),
],
),
// Black Section
const SizedBox(height: 100),
Container(
color: Colors.white,
width: double.infinity,
child: Column(
children: [
// Title Section
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24.0),
child: Column(
children: [
Text(
'Tukar ${AppConstant.poinName} buat seru-seruan',
style: AppStyle.h4.copyWith(
color: AppColor.textPrimary,
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center,
),
],
),
),
const SizedBox(height: 40),
// PageView - Show 2 items per page
SizedBox(
height: 340,
child: PageView(
controller: _pageController,
onPageChanged: (index) {
setState(() {
_currentPage = index;
});
},
children: [
// Page 1 - 2 cards
Row(
children: [
Expanded(
child: _buildRewardCard(
title: 'Main Gift Arena\nsekarang!',
subtitle: 'Dapetin s.d. 1jt Coins!',
buttonText: 'Pakai 50 💎',
isWheel: true,
),
),
Expanded(
child: _buildRewardCard(
title: 'Putar untuk\nHarapan',
subtitle: 'GoPay Pet',
buttonText: 'Pakai 5 💎',
isWheel: false,
),
),
],
),
// Page 2 - 2 cards
Row(
children: [
Expanded(
child: _buildRewardCard(
title: 'Spin & Win\nHadiah!',
subtitle: 'Kesempatan menang besar!',
buttonText: 'Pakai 30 💎',
isWheel: true,
),
),
Expanded(
child: _buildRewardCard(
title: 'Lucky Draw\nBerhadiah',
subtitle: 'Coba keberuntungan!',
buttonText: 'Pakai 20 💎',
isWheel: false,
),
),
],
),
// Page 3 - 1 card
Row(
children: [
Expanded(
child: _buildRewardCard(
title: 'Mega Prize\nWheel',
subtitle: 'Hadiah hingga 10jt!',
buttonText: 'Pakai 100 💎',
isWheel: true,
),
),
const Spacer(), // Empty space for alignment
],
),
],
),
),
const SizedBox(height: 20),
// Indicators - 3 pages
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: List.generate(3, (index) {
return AnimatedContainer(
duration: const Duration(milliseconds: 300),
margin: const EdgeInsets.symmetric(horizontal: 4),
width: _currentPage == index ? 28 : 8,
height: 8,
decoration: BoxDecoration(
color: _currentPage == index
? AppColor.primary
: Colors.grey.withOpacity(0.3),
borderRadius: BorderRadius.circular(4),
),
);
}),
),
const SizedBox(height: 40),
// Bottom Text
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24.0),
child: Column(
children: [
Text(
'Ada banyak cara dapet ${AppConstant.poinName}',
style: AppStyle.h5.copyWith(
color: AppColor.textPrimary,
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
Text(
'Mulai dari main game sampai nonton video.',
style: AppStyle.sm.copyWith(
color: AppColor.textLight,
),
textAlign: TextAlign.center,
),
],
),
),
const SizedBox(height: 60),
],
),
),
],
),
),
);
}
Widget _buildRewardCard({
required String title,
required String subtitle,
required String buttonText,
required bool isWheel,
}) {
return Padding(
padding: const EdgeInsets.only(left: 16),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 20),
decoration: BoxDecoration(
color: const Color(0xFF2D2D2D),
borderRadius: BorderRadius.circular(24),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Icon
Container(
width: 120,
height: 120,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: isWheel
? const Color(0xFF4FC3F7)
: const Color(0xFFFFC107),
boxShadow: [
BoxShadow(
color:
(isWheel
? const Color(0xFF4FC3F7)
: const Color(0xFFFFC107))
.withOpacity(0.3),
blurRadius: 20,
spreadRadius: 5,
),
],
),
child: Center(
child: Container(
width: 120,
height: 120,
decoration: const BoxDecoration(
color: Colors.white,
shape: BoxShape.circle,
),
child: isWheel
? CustomPaint(painter: WheelPainter())
: CustomPaint(painter: CoinPainter()),
),
),
),
const SizedBox(height: 20),
Text(
title,
style: AppStyle.md.copyWith(
color: AppColor.white,
fontWeight: FontWeight.bold,
height: 1.3,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 6),
Text(
subtitle,
style: AppStyle.sm.copyWith(color: AppColor.textLight),
textAlign: TextAlign.center,
),
const SizedBox(height: 18),
ElevatedButton(
onPressed: () {},
style: ElevatedButton.styleFrom(
backgroundColor: AppColor.primary,
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 12,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
elevation: 0,
),
child: Text(
buttonText,
style: AppStyle.sm.copyWith(
color: AppColor.white,
fontWeight: FontWeight.bold,
),
),
),
],
),
),
);
}
}
class WheelPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final center = Offset(size.width / 2, size.height / 2);
final radius = size.width / 2;
// Draw wheel segments
final paint = Paint()..style = PaintingStyle.fill;
const segments = 8;
final sweepAngle = (2 * math.pi) / segments;
for (int i = 0; i < segments; i++) {
paint.color = i.isEven
? const Color(0xFF2196F3)
: const Color(0xFF90CAF9);
canvas.drawArc(
Rect.fromCircle(center: center, radius: radius * 0.9),
i * sweepAngle,
sweepAngle,
true,
paint,
);
}
// Draw center circle
paint.color = const Color(0xFFFFEB3B);
canvas.drawCircle(center, radius * 0.25, paint);
// Draw pointer at top
final pointerPaint = Paint()
..color = const Color(0xFFE91E63)
..style = PaintingStyle.fill;
final pointerPath = Path();
pointerPath.moveTo(center.dx, radius * 0.15);
pointerPath.lineTo(center.dx - radius * 0.15, 0);
pointerPath.lineTo(center.dx + radius * 0.15, 0);
pointerPath.close();
canvas.drawPath(pointerPath, pointerPaint);
// Draw white border on pointer
final borderPaint = Paint()
..color = Colors.white
..style = PaintingStyle.stroke
..strokeWidth = 3;
canvas.drawPath(pointerPath, borderPaint);
// Draw dots around edge
paint.color = const Color(0xFF4FC3F7);
for (int i = 0; i < 12; i++) {
final angle = (i * 2 * math.pi) / 12;
final x = center.dx + radius * 0.95 * math.cos(angle);
final y = center.dy + radius * 0.95 * math.sin(angle);
canvas.drawCircle(Offset(x, y), 4, paint);
}
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}
class CoinPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final center = Offset(size.width / 2, size.height / 2);
final radius = size.width / 2;
// Draw outer ring
final ringPaint = Paint()
..color = const Color(0xFFD4A418)
..style = PaintingStyle.stroke
..strokeWidth = 8;
canvas.drawCircle(center, radius * 0.85, ringPaint);
// Draw inner circle
final innerPaint = Paint()
..color = const Color(0xFFFFC107)
..style = PaintingStyle.fill;
canvas.drawCircle(center, radius * 0.75, innerPaint);
// Draw center ring symbol
final centerRingPaint = Paint()
..color = const Color(0xFFD4A418)
..style = PaintingStyle.stroke
..strokeWidth = 6;
canvas.drawCircle(center, radius * 0.35, centerRingPaint);
// Draw small colored dots around
final colors = [
Colors.red,
Colors.blue,
Colors.green,
Colors.purple,
Colors.orange,
Colors.pink,
];
for (int i = 0; i < 6; i++) {
final angle = (i * 2 * math.pi) / 6;
final x = center.dx + radius * 0.6 * math.cos(angle);
final y = center.dy + radius * 0.6 * math.sin(angle);
final dotPaint = Paint()
..color = colors[i]
..style = PaintingStyle.fill;
canvas.drawCircle(Offset(x, y), 6, dotPaint);
}
// Draw numbers around edge
final textPainter = TextPainter(
textDirection: TextDirection.ltr,
textAlign: TextAlign.center,
);
for (int i = 0; i < 12; i++) {
final angle = (i * 2 * math.pi) / 12 - math.pi / 2;
final x = center.dx + radius * 0.85 * math.cos(angle);
final y = center.dy + radius * 0.85 * math.sin(angle);
textPainter.text = TextSpan(
text: '${(i + 1) * 10}',
style: const TextStyle(
color: Color(0xFFD4A418),
fontSize: 10,
fontWeight: FontWeight.bold,
),
);
textPainter.layout();
textPainter.paint(
canvas,
Offset(x - textPainter.width / 2, y - textPainter.height / 2),
);
}
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}
+13 -2
View File
@@ -35,8 +35,11 @@ class AppRouter extends RootStackRouter {
AutoRoute(page: MerchantDetailRoute.page),
// Point
AutoRoute(page: PoinRoute.page),
AutoRoute(page: PoinHistoryRoute.page),
AutoRoute(page: PointRoute.page),
// Coint
AutoRoute(page: CoinRoute.page),
AutoRoute(page: CoinHistoryRoute.page),
AutoRoute(page: ProductRedeemRoute.page),
// Draw
@@ -70,5 +73,13 @@ class AppRouter extends RootStackRouter {
// Mini Games
AutoRoute(page: FerrisWheelRoute.page),
AutoRoute(page: MisteryBoxRoute.page),
// Menu
AutoRoute(page: MenuRoute.page),
AutoRoute(page: MenuDetailRoute.page),
// Checkout
AutoRoute(page: CheckoutRoute.page),
];
}
File diff suppressed because it is too large Load Diff
+121
View File
@@ -0,0 +1,121 @@
// Models
class ProductCategory {
final String id;
final String name;
final String icon;
final int productCount;
ProductCategory({
required this.id,
required this.name,
required this.icon,
required this.productCount,
});
}
class Product {
final String id;
final String name;
final String description;
final int price;
final String categoryId;
final String imageUrl;
final bool isAvailable;
final double rating;
final int soldCount;
Product({
required this.id,
required this.name,
required this.description,
required this.price,
required this.categoryId,
required this.imageUrl,
required this.isAvailable,
required this.rating,
required this.soldCount,
});
}
// Sample data
final List<ProductCategory> categories = [
ProductCategory(id: "1", name: "Makanan", icon: "🍽️", productCount: 8),
ProductCategory(id: "2", name: "Minuman", icon: "🥤", productCount: 6),
ProductCategory(id: "3", name: "Snack", icon: "🍿", productCount: 5),
ProductCategory(id: "4", name: "Es Krim", icon: "🍦", productCount: 4),
ProductCategory(id: "5", name: "Paket", icon: "📦", productCount: 3),
];
final List<Product> products = [
// Makanan
Product(
id: "1",
name: "Nasi Gudeg",
description: "Gudeg khas Yogyakarta dengan ayam dan telur",
price: 25000,
categoryId: "1",
imageUrl: "https://via.placeholder.com/150",
isAvailable: true,
rating: 4.5,
soldCount: 50,
),
Product(
id: "2",
name: "Soto Ayam",
description: "Soto ayam kuning dengan nasi dan kerupuk",
price: 18000,
categoryId: "1",
imageUrl: "https://via.placeholder.com/150",
isAvailable: true,
rating: 4.3,
soldCount: 75,
),
Product(
id: "3",
name: "Gado-gado",
description: "Gado-gado segar dengan bumbu kacang",
price: 15000,
categoryId: "1",
imageUrl: "https://via.placeholder.com/150",
isAvailable: false,
rating: 4.2,
soldCount: 30,
),
// Minuman
Product(
id: "4",
name: "Es Teh Manis",
description: "Es teh manis segar",
price: 5000,
categoryId: "2",
imageUrl: "https://via.placeholder.com/150",
isAvailable: true,
rating: 4.0,
soldCount: 120,
),
Product(
id: "5",
name: "Jus Jeruk",
description: "Jus jeruk segar tanpa gula tambahan",
price: 12000,
categoryId: "2",
imageUrl: "https://via.placeholder.com/150",
isAvailable: true,
rating: 4.4,
soldCount: 45,
),
// Snack
Product(
id: "6",
name: "Keripik Pisang",
description: "Keripik pisang renyah dan manis",
price: 8000,
categoryId: "3",
imageUrl: "https://via.placeholder.com/150",
isAvailable: true,
rating: 4.1,
soldCount: 25,
),
];
+1 -1
View File
@@ -1,4 +1,4 @@
platform :osx, '10.14'
platform :osx, '10.15'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
+3 -3
View File
@@ -461,7 +461,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.14;
MACOSX_DEPLOYMENT_TARGET = 10.15;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = macosx;
SWIFT_COMPILATION_MODE = wholemodule;
@@ -543,7 +543,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.14;
MACOSX_DEPLOYMENT_TARGET = 10.15;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = macosx;
@@ -593,7 +593,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.14;
MACOSX_DEPLOYMENT_TARGET = 10.15;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = macosx;
SWIFT_COMPILATION_MODE = wholemodule;
+12 -12
View File
@@ -668,26 +668,26 @@ packages:
dependency: transitive
description:
name: leak_tracker
sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0"
sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de"
url: "https://pub.dev"
source: hosted
version: "10.0.9"
version: "11.0.2"
leak_tracker_flutter_testing:
dependency: transitive
description:
name: leak_tracker_flutter_testing
sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573
sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1"
url: "https://pub.dev"
source: hosted
version: "3.0.9"
version: "3.0.10"
leak_tracker_testing:
dependency: transitive
description:
name: leak_tracker_testing
sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3"
sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1"
url: "https://pub.dev"
source: hosted
version: "3.0.1"
version: "3.0.2"
lints:
dependency: transitive
description:
@@ -724,10 +724,10 @@ packages:
dependency: transitive
description:
name: meta
sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
url: "https://pub.dev"
source: hosted
version: "1.16.0"
version: "1.17.0"
mime:
dependency: transitive
description:
@@ -1121,10 +1121,10 @@ packages:
dependency: transitive
description:
name: test_api
sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd
sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55
url: "https://pub.dev"
source: hosted
version: "0.7.4"
version: "0.7.7"
time:
dependency: transitive
description:
@@ -1249,10 +1249,10 @@ packages:
dependency: transitive
description:
name: vector_math
sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803"
sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b
url: "https://pub.dev"
source: hosted
version: "2.1.4"
version: "2.2.0"
vm_service:
dependency: transitive
description: