Compare commits
7
Commits
73918430b2
..
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7887704a5c | ||
|
|
4c12244d3c | ||
|
|
f4f775b9ed | ||
|
|
44d48d41d4 | ||
|
|
82e1f93cce | ||
|
|
1ce980a87b | ||
|
|
909c312af0 |
Binary file not shown.
|
After Width: | Height: | Size: 332 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 367 KiB |
@@ -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,40 +0,0 @@
|
||||
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 'game_prize_loader_event.dart';
|
||||
part 'game_prize_loader_state.dart';
|
||||
part 'game_prize_loader_bloc.freezed.dart';
|
||||
|
||||
@injectable
|
||||
class GamePrizeLoaderBloc
|
||||
extends Bloc<GamePrizeLoaderEvent, GamePrizeLoaderState> {
|
||||
final IGameRepository _repository;
|
||||
GamePrizeLoaderBloc(this._repository)
|
||||
: super(GamePrizeLoaderState.initial()) {
|
||||
on<GamePrizeLoaderEvent>(_onGamePrizeLoaderEvent);
|
||||
}
|
||||
|
||||
Future<void> _onGamePrizeLoaderEvent(
|
||||
GamePrizeLoaderEvent event,
|
||||
Emitter<GamePrizeLoaderState> emit,
|
||||
) {
|
||||
return event.map(
|
||||
fetched: (e) async {
|
||||
emit(state.copyWith(isFetching: true, failureOptionGamePrize: none()));
|
||||
|
||||
final result = await _repository.gamePrizeByGameId(id: e.id);
|
||||
|
||||
var data = result.fold(
|
||||
(f) => state.copyWith(failureOptionGamePrize: optionOf(f)),
|
||||
(gamePrize) => state.copyWith(gamePrize: gamePrize),
|
||||
);
|
||||
|
||||
emit(data.copyWith(isFetching: false));
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,442 +0,0 @@
|
||||
// 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_prize_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 _$GamePrizeLoaderEvent {
|
||||
String get id => throw _privateConstructorUsedError;
|
||||
@optionalTypeArgs
|
||||
TResult when<TResult extends Object?>({
|
||||
required TResult Function(String id) fetched,
|
||||
}) => throw _privateConstructorUsedError;
|
||||
@optionalTypeArgs
|
||||
TResult? whenOrNull<TResult extends Object?>({
|
||||
TResult? Function(String id)? fetched,
|
||||
}) => throw _privateConstructorUsedError;
|
||||
@optionalTypeArgs
|
||||
TResult maybeWhen<TResult extends Object?>({
|
||||
TResult Function(String id)? 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;
|
||||
|
||||
/// Create a copy of GamePrizeLoaderEvent
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
$GamePrizeLoaderEventCopyWith<GamePrizeLoaderEvent> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class $GamePrizeLoaderEventCopyWith<$Res> {
|
||||
factory $GamePrizeLoaderEventCopyWith(
|
||||
GamePrizeLoaderEvent value,
|
||||
$Res Function(GamePrizeLoaderEvent) then,
|
||||
) = _$GamePrizeLoaderEventCopyWithImpl<$Res, GamePrizeLoaderEvent>;
|
||||
@useResult
|
||||
$Res call({String id});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$GamePrizeLoaderEventCopyWithImpl<
|
||||
$Res,
|
||||
$Val extends GamePrizeLoaderEvent
|
||||
>
|
||||
implements $GamePrizeLoaderEventCopyWith<$Res> {
|
||||
_$GamePrizeLoaderEventCopyWithImpl(this._value, this._then);
|
||||
|
||||
// ignore: unused_field
|
||||
final $Val _value;
|
||||
// ignore: unused_field
|
||||
final $Res Function($Val) _then;
|
||||
|
||||
/// Create a copy of GamePrizeLoaderEvent
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({Object? id = null}) {
|
||||
return _then(
|
||||
_value.copyWith(
|
||||
id: null == id
|
||||
? _value.id
|
||||
: id // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
)
|
||||
as $Val,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class _$$FetchedImplCopyWith<$Res>
|
||||
implements $GamePrizeLoaderEventCopyWith<$Res> {
|
||||
factory _$$FetchedImplCopyWith(
|
||||
_$FetchedImpl value,
|
||||
$Res Function(_$FetchedImpl) then,
|
||||
) = __$$FetchedImplCopyWithImpl<$Res>;
|
||||
@override
|
||||
@useResult
|
||||
$Res call({String id});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class __$$FetchedImplCopyWithImpl<$Res>
|
||||
extends _$GamePrizeLoaderEventCopyWithImpl<$Res, _$FetchedImpl>
|
||||
implements _$$FetchedImplCopyWith<$Res> {
|
||||
__$$FetchedImplCopyWithImpl(
|
||||
_$FetchedImpl _value,
|
||||
$Res Function(_$FetchedImpl) _then,
|
||||
) : super(_value, _then);
|
||||
|
||||
/// Create a copy of GamePrizeLoaderEvent
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({Object? id = null}) {
|
||||
return _then(
|
||||
_$FetchedImpl(
|
||||
null == id
|
||||
? _value.id
|
||||
: id // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
|
||||
class _$FetchedImpl implements _Fetched {
|
||||
const _$FetchedImpl(this.id);
|
||||
|
||||
@override
|
||||
final String id;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'GamePrizeLoaderEvent.fetched(id: $id)';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _$FetchedImpl &&
|
||||
(identical(other.id, id) || other.id == id));
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType, id);
|
||||
|
||||
/// Create a copy of GamePrizeLoaderEvent
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
_$$FetchedImplCopyWith<_$FetchedImpl> get copyWith =>
|
||||
__$$FetchedImplCopyWithImpl<_$FetchedImpl>(this, _$identity);
|
||||
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult when<TResult extends Object?>({
|
||||
required TResult Function(String id) fetched,
|
||||
}) {
|
||||
return fetched(id);
|
||||
}
|
||||
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult? whenOrNull<TResult extends Object?>({
|
||||
TResult? Function(String id)? fetched,
|
||||
}) {
|
||||
return fetched?.call(id);
|
||||
}
|
||||
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult maybeWhen<TResult extends Object?>({
|
||||
TResult Function(String id)? fetched,
|
||||
required TResult orElse(),
|
||||
}) {
|
||||
if (fetched != null) {
|
||||
return fetched(id);
|
||||
}
|
||||
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 GamePrizeLoaderEvent {
|
||||
const factory _Fetched(final String id) = _$FetchedImpl;
|
||||
|
||||
@override
|
||||
String get id;
|
||||
|
||||
/// Create a copy of GamePrizeLoaderEvent
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
_$$FetchedImplCopyWith<_$FetchedImpl> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
mixin _$GamePrizeLoaderState {
|
||||
List<GamePrize> get gamePrize => throw _privateConstructorUsedError;
|
||||
Option<GameFailure> get failureOptionGamePrize =>
|
||||
throw _privateConstructorUsedError;
|
||||
bool get isFetching => throw _privateConstructorUsedError;
|
||||
|
||||
/// Create a copy of GamePrizeLoaderState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
$GamePrizeLoaderStateCopyWith<GamePrizeLoaderState> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class $GamePrizeLoaderStateCopyWith<$Res> {
|
||||
factory $GamePrizeLoaderStateCopyWith(
|
||||
GamePrizeLoaderState value,
|
||||
$Res Function(GamePrizeLoaderState) then,
|
||||
) = _$GamePrizeLoaderStateCopyWithImpl<$Res, GamePrizeLoaderState>;
|
||||
@useResult
|
||||
$Res call({
|
||||
List<GamePrize> gamePrize,
|
||||
Option<GameFailure> failureOptionGamePrize,
|
||||
bool isFetching,
|
||||
});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$GamePrizeLoaderStateCopyWithImpl<
|
||||
$Res,
|
||||
$Val extends GamePrizeLoaderState
|
||||
>
|
||||
implements $GamePrizeLoaderStateCopyWith<$Res> {
|
||||
_$GamePrizeLoaderStateCopyWithImpl(this._value, this._then);
|
||||
|
||||
// ignore: unused_field
|
||||
final $Val _value;
|
||||
// ignore: unused_field
|
||||
final $Res Function($Val) _then;
|
||||
|
||||
/// Create a copy of GamePrizeLoaderState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? gamePrize = null,
|
||||
Object? failureOptionGamePrize = null,
|
||||
Object? isFetching = null,
|
||||
}) {
|
||||
return _then(
|
||||
_value.copyWith(
|
||||
gamePrize: null == gamePrize
|
||||
? _value.gamePrize
|
||||
: gamePrize // ignore: cast_nullable_to_non_nullable
|
||||
as List<GamePrize>,
|
||||
failureOptionGamePrize: null == failureOptionGamePrize
|
||||
? _value.failureOptionGamePrize
|
||||
: failureOptionGamePrize // 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class _$$GamePrizeLoaderStateImplCopyWith<$Res>
|
||||
implements $GamePrizeLoaderStateCopyWith<$Res> {
|
||||
factory _$$GamePrizeLoaderStateImplCopyWith(
|
||||
_$GamePrizeLoaderStateImpl value,
|
||||
$Res Function(_$GamePrizeLoaderStateImpl) then,
|
||||
) = __$$GamePrizeLoaderStateImplCopyWithImpl<$Res>;
|
||||
@override
|
||||
@useResult
|
||||
$Res call({
|
||||
List<GamePrize> gamePrize,
|
||||
Option<GameFailure> failureOptionGamePrize,
|
||||
bool isFetching,
|
||||
});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class __$$GamePrizeLoaderStateImplCopyWithImpl<$Res>
|
||||
extends _$GamePrizeLoaderStateCopyWithImpl<$Res, _$GamePrizeLoaderStateImpl>
|
||||
implements _$$GamePrizeLoaderStateImplCopyWith<$Res> {
|
||||
__$$GamePrizeLoaderStateImplCopyWithImpl(
|
||||
_$GamePrizeLoaderStateImpl _value,
|
||||
$Res Function(_$GamePrizeLoaderStateImpl) _then,
|
||||
) : super(_value, _then);
|
||||
|
||||
/// Create a copy of GamePrizeLoaderState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? gamePrize = null,
|
||||
Object? failureOptionGamePrize = null,
|
||||
Object? isFetching = null,
|
||||
}) {
|
||||
return _then(
|
||||
_$GamePrizeLoaderStateImpl(
|
||||
gamePrize: null == gamePrize
|
||||
? _value._gamePrize
|
||||
: gamePrize // ignore: cast_nullable_to_non_nullable
|
||||
as List<GamePrize>,
|
||||
failureOptionGamePrize: null == failureOptionGamePrize
|
||||
? _value.failureOptionGamePrize
|
||||
: failureOptionGamePrize // 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 _$GamePrizeLoaderStateImpl implements _GamePrizeLoaderState {
|
||||
const _$GamePrizeLoaderStateImpl({
|
||||
required final List<GamePrize> gamePrize,
|
||||
required this.failureOptionGamePrize,
|
||||
this.isFetching = false,
|
||||
}) : _gamePrize = gamePrize;
|
||||
|
||||
final List<GamePrize> _gamePrize;
|
||||
@override
|
||||
List<GamePrize> get gamePrize {
|
||||
if (_gamePrize is EqualUnmodifiableListView) return _gamePrize;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableListView(_gamePrize);
|
||||
}
|
||||
|
||||
@override
|
||||
final Option<GameFailure> failureOptionGamePrize;
|
||||
@override
|
||||
@JsonKey()
|
||||
final bool isFetching;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'GamePrizeLoaderState(gamePrize: $gamePrize, failureOptionGamePrize: $failureOptionGamePrize, isFetching: $isFetching)';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _$GamePrizeLoaderStateImpl &&
|
||||
const DeepCollectionEquality().equals(
|
||||
other._gamePrize,
|
||||
_gamePrize,
|
||||
) &&
|
||||
(identical(other.failureOptionGamePrize, failureOptionGamePrize) ||
|
||||
other.failureOptionGamePrize == failureOptionGamePrize) &&
|
||||
(identical(other.isFetching, isFetching) ||
|
||||
other.isFetching == isFetching));
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(
|
||||
runtimeType,
|
||||
const DeepCollectionEquality().hash(_gamePrize),
|
||||
failureOptionGamePrize,
|
||||
isFetching,
|
||||
);
|
||||
|
||||
/// Create a copy of GamePrizeLoaderState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
_$$GamePrizeLoaderStateImplCopyWith<_$GamePrizeLoaderStateImpl>
|
||||
get copyWith =>
|
||||
__$$GamePrizeLoaderStateImplCopyWithImpl<_$GamePrizeLoaderStateImpl>(
|
||||
this,
|
||||
_$identity,
|
||||
);
|
||||
}
|
||||
|
||||
abstract class _GamePrizeLoaderState implements GamePrizeLoaderState {
|
||||
const factory _GamePrizeLoaderState({
|
||||
required final List<GamePrize> gamePrize,
|
||||
required final Option<GameFailure> failureOptionGamePrize,
|
||||
final bool isFetching,
|
||||
}) = _$GamePrizeLoaderStateImpl;
|
||||
|
||||
@override
|
||||
List<GamePrize> get gamePrize;
|
||||
@override
|
||||
Option<GameFailure> get failureOptionGamePrize;
|
||||
@override
|
||||
bool get isFetching;
|
||||
|
||||
/// Create a copy of GamePrizeLoaderState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
_$$GamePrizeLoaderStateImplCopyWith<_$GamePrizeLoaderStateImpl>
|
||||
get copyWith => throw _privateConstructorUsedError;
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
part of 'game_prize_loader_bloc.dart';
|
||||
|
||||
@freezed
|
||||
class GamePrizeLoaderEvent with _$GamePrizeLoaderEvent {
|
||||
const factory GamePrizeLoaderEvent.fetched(String id) = _Fetched;
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
part of 'game_prize_loader_bloc.dart';
|
||||
|
||||
@freezed
|
||||
class GamePrizeLoaderState with _$GamePrizeLoaderState {
|
||||
const factory GamePrizeLoaderState({
|
||||
required List<GamePrize> gamePrize,
|
||||
required Option<GameFailure> failureOptionGamePrize,
|
||||
@Default(false) bool isFetching,
|
||||
}) = _GamePrizeLoaderState;
|
||||
|
||||
factory GamePrizeLoaderState.initial() =>
|
||||
GamePrizeLoaderState(gamePrize: [], failureOptionGamePrize: none());
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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() {
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
// wheel_painter.dart - Fixed implementation with consistent positioning
|
||||
import 'dart:math' as math;
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../domain/game/game.dart';
|
||||
import '../theme/theme.dart';
|
||||
|
||||
class WheelPainter extends CustomPainter {
|
||||
final List<GamePrize> gamePrizes;
|
||||
@@ -36,10 +35,11 @@ class WheelPainter extends CustomPainter {
|
||||
..style = PaintingStyle.fill;
|
||||
canvas.drawCircle(center, radius - 20, innerWhitePaint);
|
||||
|
||||
// Draw sections
|
||||
// Draw sections - KONSISTEN dengan logic spin
|
||||
for (int i = 0; i < gamePrizes.length; i++) {
|
||||
final prize = gamePrizes[i];
|
||||
final startAngle = i * sectionAngle - math.pi / 2;
|
||||
// Section 0 di top (-π/2), section 1 di kanan atas, dst (clockwise)
|
||||
final startAngle = (-math.pi / 2) + (i * sectionAngle);
|
||||
|
||||
// Section background
|
||||
final sectionPaint = Paint()
|
||||
@@ -54,54 +54,13 @@ 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);
|
||||
|
||||
// Get icon from metadata or use default
|
||||
final iconData = _getIconFromMetadata(prize.metadata);
|
||||
final iconText = _getIconText(iconData);
|
||||
final iconTextPainter = TextPainter(
|
||||
text: TextSpan(
|
||||
text: iconText,
|
||||
style: TextStyle(
|
||||
fontFamily: 'MaterialIcons',
|
||||
fontSize: 20,
|
||||
color: getPrizeColor(prize, i),
|
||||
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);
|
||||
@@ -111,62 +70,51 @@ class WheelPainter extends CustomPainter {
|
||||
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 stock indicator if stock is low
|
||||
if (prize.stock <= 5 && prize.stock > 0) {
|
||||
final stockAngle = startAngle + sectionAngle / 2;
|
||||
final stockPosition = Offset(
|
||||
center.dx + (radius - 35) * math.cos(stockAngle),
|
||||
center.dy + (radius - 35) * math.sin(stockAngle),
|
||||
);
|
||||
|
||||
final stockPaint = Paint()
|
||||
..color = AppColor.error
|
||||
..style = PaintingStyle.fill;
|
||||
canvas.drawCircle(stockPosition, 8, stockPaint);
|
||||
|
||||
canvas.save();
|
||||
canvas.translate(stockPosition.dx, stockPosition.dy);
|
||||
|
||||
final stockTextPainter = TextPainter(
|
||||
text: TextSpan(
|
||||
text: '!',
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
textDirection: TextDirection.ltr,
|
||||
);
|
||||
stockTextPainter.layout();
|
||||
stockTextPainter.paint(
|
||||
canvas,
|
||||
Offset(-stockTextPainter.width / 2, -stockTextPainter.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(
|
||||
@@ -178,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 < gamePrizes.length; i++) {
|
||||
final angle = i * sectionAngle - math.pi / 2;
|
||||
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),
|
||||
@@ -196,64 +143,6 @@ class WheelPainter extends CustomPainter {
|
||||
}
|
||||
}
|
||||
|
||||
IconData _getIconFromMetadata(Map<String, dynamic> metadata) {
|
||||
final iconName = metadata['icon'] as String?;
|
||||
switch (iconName?.toLowerCase()) {
|
||||
case 'card_giftcard':
|
||||
return Icons.card_giftcard;
|
||||
case 'monetization_on':
|
||||
return Icons.monetization_on;
|
||||
case 'redeem':
|
||||
return Icons.redeem;
|
||||
case 'attach_money':
|
||||
return Icons.attach_money;
|
||||
case 'account_balance_wallet':
|
||||
return Icons.account_balance_wallet;
|
||||
case 'diamond':
|
||||
return Icons.diamond;
|
||||
case 'star':
|
||||
return Icons.star;
|
||||
case 'emoji_events':
|
||||
return Icons.emoji_events;
|
||||
case 'refresh':
|
||||
return Icons.refresh;
|
||||
case 'visibility':
|
||||
return Icons.visibility;
|
||||
default:
|
||||
return Icons.card_giftcard; // Default icon
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
case 0xe5ca: // Icons.diamond
|
||||
return String.fromCharCode(0xe5ca);
|
||||
case 0xe838: // Icons.star
|
||||
return String.fromCharCode(0xe838);
|
||||
case 0xe2a3: // Icons.emoji_events
|
||||
return String.fromCharCode(0xe2a3);
|
||||
default:
|
||||
return String.fromCharCode(0xe151); // Default card_giftcard icon
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -7,7 +7,8 @@ class ApiPath {
|
||||
static String resend = '/api/v1/customer-auth/resend-otp';
|
||||
|
||||
// Marketing
|
||||
static String gamePrizeByGameId = '/api/v1/marketing/game-prizes/game';
|
||||
static String ferrisWheel = '/api/v1/customer/ferris-wheel';
|
||||
|
||||
// Customer
|
||||
static String customerPoint = '/api/v1/customer/points';
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ class Game with _$Game {
|
||||
required String type,
|
||||
required bool isActive,
|
||||
required Map<String, dynamic> metadata,
|
||||
required List<GamePrize> prizes,
|
||||
required String createdAt,
|
||||
required String updatedAt,
|
||||
}) = _Game;
|
||||
@@ -18,6 +19,7 @@ class Game with _$Game {
|
||||
type: '',
|
||||
isActive: false,
|
||||
metadata: {},
|
||||
prizes: [],
|
||||
createdAt: '',
|
||||
updatedAt: '',
|
||||
);
|
||||
|
||||
@@ -6,26 +6,16 @@ class GamePrize with _$GamePrize {
|
||||
required String id,
|
||||
required String gameId,
|
||||
required String name,
|
||||
required int weight,
|
||||
required int stock,
|
||||
required int maxStock,
|
||||
required int threshold,
|
||||
required Map<String, dynamic> metadata,
|
||||
required Game game,
|
||||
required String createdAt,
|
||||
required String updatedAt,
|
||||
}) = _GamePrize;
|
||||
|
||||
factory GamePrize.empty() => GamePrize(
|
||||
factory GamePrize.empty() => const GamePrize(
|
||||
id: '',
|
||||
gameId: '',
|
||||
name: '',
|
||||
weight: 0,
|
||||
stock: 0,
|
||||
maxStock: 0,
|
||||
threshold: 0,
|
||||
metadata: const {},
|
||||
game: Game.empty(),
|
||||
metadata: {},
|
||||
createdAt: '',
|
||||
updatedAt: '',
|
||||
);
|
||||
|
||||
@@ -22,6 +22,7 @@ mixin _$Game {
|
||||
String get type => throw _privateConstructorUsedError;
|
||||
bool get isActive => throw _privateConstructorUsedError;
|
||||
Map<String, dynamic> get metadata => throw _privateConstructorUsedError;
|
||||
List<GamePrize> get prizes => throw _privateConstructorUsedError;
|
||||
String get createdAt => throw _privateConstructorUsedError;
|
||||
String get updatedAt => throw _privateConstructorUsedError;
|
||||
|
||||
@@ -42,6 +43,7 @@ abstract class $GameCopyWith<$Res> {
|
||||
String type,
|
||||
bool isActive,
|
||||
Map<String, dynamic> metadata,
|
||||
List<GamePrize> prizes,
|
||||
String createdAt,
|
||||
String updatedAt,
|
||||
});
|
||||
@@ -67,6 +69,7 @@ class _$GameCopyWithImpl<$Res, $Val extends Game>
|
||||
Object? type = null,
|
||||
Object? isActive = null,
|
||||
Object? metadata = null,
|
||||
Object? prizes = null,
|
||||
Object? createdAt = null,
|
||||
Object? updatedAt = null,
|
||||
}) {
|
||||
@@ -92,6 +95,10 @@ class _$GameCopyWithImpl<$Res, $Val extends Game>
|
||||
? _value.metadata
|
||||
: metadata // ignore: cast_nullable_to_non_nullable
|
||||
as Map<String, dynamic>,
|
||||
prizes: null == prizes
|
||||
? _value.prizes
|
||||
: prizes // ignore: cast_nullable_to_non_nullable
|
||||
as List<GamePrize>,
|
||||
createdAt: null == createdAt
|
||||
? _value.createdAt
|
||||
: createdAt // ignore: cast_nullable_to_non_nullable
|
||||
@@ -120,6 +127,7 @@ abstract class _$$GameImplCopyWith<$Res> implements $GameCopyWith<$Res> {
|
||||
String type,
|
||||
bool isActive,
|
||||
Map<String, dynamic> metadata,
|
||||
List<GamePrize> prizes,
|
||||
String createdAt,
|
||||
String updatedAt,
|
||||
});
|
||||
@@ -142,6 +150,7 @@ class __$$GameImplCopyWithImpl<$Res>
|
||||
Object? type = null,
|
||||
Object? isActive = null,
|
||||
Object? metadata = null,
|
||||
Object? prizes = null,
|
||||
Object? createdAt = null,
|
||||
Object? updatedAt = null,
|
||||
}) {
|
||||
@@ -167,6 +176,10 @@ class __$$GameImplCopyWithImpl<$Res>
|
||||
? _value._metadata
|
||||
: metadata // ignore: cast_nullable_to_non_nullable
|
||||
as Map<String, dynamic>,
|
||||
prizes: null == prizes
|
||||
? _value._prizes
|
||||
: prizes // ignore: cast_nullable_to_non_nullable
|
||||
as List<GamePrize>,
|
||||
createdAt: null == createdAt
|
||||
? _value.createdAt
|
||||
: createdAt // ignore: cast_nullable_to_non_nullable
|
||||
@@ -189,9 +202,11 @@ class _$GameImpl implements _Game {
|
||||
required this.type,
|
||||
required this.isActive,
|
||||
required final Map<String, dynamic> metadata,
|
||||
required final List<GamePrize> prizes,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
}) : _metadata = metadata;
|
||||
}) : _metadata = metadata,
|
||||
_prizes = prizes;
|
||||
|
||||
@override
|
||||
final String id;
|
||||
@@ -209,6 +224,14 @@ class _$GameImpl implements _Game {
|
||||
return EqualUnmodifiableMapView(_metadata);
|
||||
}
|
||||
|
||||
final List<GamePrize> _prizes;
|
||||
@override
|
||||
List<GamePrize> get prizes {
|
||||
if (_prizes is EqualUnmodifiableListView) return _prizes;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableListView(_prizes);
|
||||
}
|
||||
|
||||
@override
|
||||
final String createdAt;
|
||||
@override
|
||||
@@ -216,7 +239,7 @@ class _$GameImpl implements _Game {
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'Game(id: $id, name: $name, type: $type, isActive: $isActive, metadata: $metadata, createdAt: $createdAt, updatedAt: $updatedAt)';
|
||||
return 'Game(id: $id, name: $name, type: $type, isActive: $isActive, metadata: $metadata, prizes: $prizes, createdAt: $createdAt, updatedAt: $updatedAt)';
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -230,6 +253,7 @@ class _$GameImpl implements _Game {
|
||||
(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) ||
|
||||
@@ -244,6 +268,7 @@ class _$GameImpl implements _Game {
|
||||
type,
|
||||
isActive,
|
||||
const DeepCollectionEquality().hash(_metadata),
|
||||
const DeepCollectionEquality().hash(_prizes),
|
||||
createdAt,
|
||||
updatedAt,
|
||||
);
|
||||
@@ -264,6 +289,7 @@ abstract class _Game implements Game {
|
||||
required final String type,
|
||||
required final bool isActive,
|
||||
required final Map<String, dynamic> metadata,
|
||||
required final List<GamePrize> prizes,
|
||||
required final String createdAt,
|
||||
required final String updatedAt,
|
||||
}) = _$GameImpl;
|
||||
@@ -279,6 +305,8 @@ abstract class _Game implements Game {
|
||||
@override
|
||||
Map<String, dynamic> get metadata;
|
||||
@override
|
||||
List<GamePrize> get prizes;
|
||||
@override
|
||||
String get createdAt;
|
||||
@override
|
||||
String get updatedAt;
|
||||
@@ -296,12 +324,7 @@ mixin _$GamePrize {
|
||||
String get id => throw _privateConstructorUsedError;
|
||||
String get gameId => throw _privateConstructorUsedError;
|
||||
String get name => throw _privateConstructorUsedError;
|
||||
int get weight => throw _privateConstructorUsedError;
|
||||
int get stock => throw _privateConstructorUsedError;
|
||||
int get maxStock => throw _privateConstructorUsedError;
|
||||
int get threshold => throw _privateConstructorUsedError;
|
||||
Map<String, dynamic> get metadata => throw _privateConstructorUsedError;
|
||||
Game get game => throw _privateConstructorUsedError;
|
||||
String get createdAt => throw _privateConstructorUsedError;
|
||||
String get updatedAt => throw _privateConstructorUsedError;
|
||||
|
||||
@@ -321,17 +344,10 @@ abstract class $GamePrizeCopyWith<$Res> {
|
||||
String id,
|
||||
String gameId,
|
||||
String name,
|
||||
int weight,
|
||||
int stock,
|
||||
int maxStock,
|
||||
int threshold,
|
||||
Map<String, dynamic> metadata,
|
||||
Game game,
|
||||
String createdAt,
|
||||
String updatedAt,
|
||||
});
|
||||
|
||||
$GameCopyWith<$Res> get game;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@@ -352,12 +368,7 @@ class _$GamePrizeCopyWithImpl<$Res, $Val extends GamePrize>
|
||||
Object? id = null,
|
||||
Object? gameId = null,
|
||||
Object? name = null,
|
||||
Object? weight = null,
|
||||
Object? stock = null,
|
||||
Object? maxStock = null,
|
||||
Object? threshold = null,
|
||||
Object? metadata = null,
|
||||
Object? game = null,
|
||||
Object? createdAt = null,
|
||||
Object? updatedAt = null,
|
||||
}) {
|
||||
@@ -375,30 +386,10 @@ class _$GamePrizeCopyWithImpl<$Res, $Val extends GamePrize>
|
||||
? _value.name
|
||||
: name // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
weight: null == weight
|
||||
? _value.weight
|
||||
: weight // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
stock: null == stock
|
||||
? _value.stock
|
||||
: stock // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
maxStock: null == maxStock
|
||||
? _value.maxStock
|
||||
: maxStock // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
threshold: null == threshold
|
||||
? _value.threshold
|
||||
: threshold // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
metadata: null == metadata
|
||||
? _value.metadata
|
||||
: metadata // ignore: cast_nullable_to_non_nullable
|
||||
as Map<String, dynamic>,
|
||||
game: null == game
|
||||
? _value.game
|
||||
: game // ignore: cast_nullable_to_non_nullable
|
||||
as Game,
|
||||
createdAt: null == createdAt
|
||||
? _value.createdAt
|
||||
: createdAt // ignore: cast_nullable_to_non_nullable
|
||||
@@ -411,16 +402,6 @@ class _$GamePrizeCopyWithImpl<$Res, $Val extends GamePrize>
|
||||
as $Val,
|
||||
);
|
||||
}
|
||||
|
||||
/// Create a copy of GamePrize
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$GameCopyWith<$Res> get game {
|
||||
return $GameCopyWith<$Res>(_value.game, (value) {
|
||||
return _then(_value.copyWith(game: value) as $Val);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@@ -436,18 +417,10 @@ abstract class _$$GamePrizeImplCopyWith<$Res>
|
||||
String id,
|
||||
String gameId,
|
||||
String name,
|
||||
int weight,
|
||||
int stock,
|
||||
int maxStock,
|
||||
int threshold,
|
||||
Map<String, dynamic> metadata,
|
||||
Game game,
|
||||
String createdAt,
|
||||
String updatedAt,
|
||||
});
|
||||
|
||||
@override
|
||||
$GameCopyWith<$Res> get game;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@@ -467,12 +440,7 @@ class __$$GamePrizeImplCopyWithImpl<$Res>
|
||||
Object? id = null,
|
||||
Object? gameId = null,
|
||||
Object? name = null,
|
||||
Object? weight = null,
|
||||
Object? stock = null,
|
||||
Object? maxStock = null,
|
||||
Object? threshold = null,
|
||||
Object? metadata = null,
|
||||
Object? game = null,
|
||||
Object? createdAt = null,
|
||||
Object? updatedAt = null,
|
||||
}) {
|
||||
@@ -490,30 +458,10 @@ class __$$GamePrizeImplCopyWithImpl<$Res>
|
||||
? _value.name
|
||||
: name // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
weight: null == weight
|
||||
? _value.weight
|
||||
: weight // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
stock: null == stock
|
||||
? _value.stock
|
||||
: stock // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
maxStock: null == maxStock
|
||||
? _value.maxStock
|
||||
: maxStock // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
threshold: null == threshold
|
||||
? _value.threshold
|
||||
: threshold // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
metadata: null == metadata
|
||||
? _value._metadata
|
||||
: metadata // ignore: cast_nullable_to_non_nullable
|
||||
as Map<String, dynamic>,
|
||||
game: null == game
|
||||
? _value.game
|
||||
: game // ignore: cast_nullable_to_non_nullable
|
||||
as Game,
|
||||
createdAt: null == createdAt
|
||||
? _value.createdAt
|
||||
: createdAt // ignore: cast_nullable_to_non_nullable
|
||||
@@ -534,12 +482,7 @@ class _$GamePrizeImpl implements _GamePrize {
|
||||
required this.id,
|
||||
required this.gameId,
|
||||
required this.name,
|
||||
required this.weight,
|
||||
required this.stock,
|
||||
required this.maxStock,
|
||||
required this.threshold,
|
||||
required final Map<String, dynamic> metadata,
|
||||
required this.game,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
}) : _metadata = metadata;
|
||||
@@ -550,14 +493,6 @@ class _$GamePrizeImpl implements _GamePrize {
|
||||
final String gameId;
|
||||
@override
|
||||
final String name;
|
||||
@override
|
||||
final int weight;
|
||||
@override
|
||||
final int stock;
|
||||
@override
|
||||
final int maxStock;
|
||||
@override
|
||||
final int threshold;
|
||||
final Map<String, dynamic> _metadata;
|
||||
@override
|
||||
Map<String, dynamic> get metadata {
|
||||
@@ -566,8 +501,6 @@ class _$GamePrizeImpl implements _GamePrize {
|
||||
return EqualUnmodifiableMapView(_metadata);
|
||||
}
|
||||
|
||||
@override
|
||||
final Game game;
|
||||
@override
|
||||
final String createdAt;
|
||||
@override
|
||||
@@ -575,7 +508,7 @@ class _$GamePrizeImpl implements _GamePrize {
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'GamePrize(id: $id, gameId: $gameId, name: $name, weight: $weight, stock: $stock, maxStock: $maxStock, threshold: $threshold, metadata: $metadata, game: $game, createdAt: $createdAt, updatedAt: $updatedAt)';
|
||||
return 'GamePrize(id: $id, gameId: $gameId, name: $name, metadata: $metadata, createdAt: $createdAt, updatedAt: $updatedAt)';
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -586,14 +519,7 @@ class _$GamePrizeImpl implements _GamePrize {
|
||||
(identical(other.id, id) || other.id == id) &&
|
||||
(identical(other.gameId, gameId) || other.gameId == gameId) &&
|
||||
(identical(other.name, name) || other.name == name) &&
|
||||
(identical(other.weight, weight) || other.weight == weight) &&
|
||||
(identical(other.stock, stock) || other.stock == stock) &&
|
||||
(identical(other.maxStock, maxStock) ||
|
||||
other.maxStock == maxStock) &&
|
||||
(identical(other.threshold, threshold) ||
|
||||
other.threshold == threshold) &&
|
||||
const DeepCollectionEquality().equals(other._metadata, _metadata) &&
|
||||
(identical(other.game, game) || other.game == game) &&
|
||||
(identical(other.createdAt, createdAt) ||
|
||||
other.createdAt == createdAt) &&
|
||||
(identical(other.updatedAt, updatedAt) ||
|
||||
@@ -606,12 +532,7 @@ class _$GamePrizeImpl implements _GamePrize {
|
||||
id,
|
||||
gameId,
|
||||
name,
|
||||
weight,
|
||||
stock,
|
||||
maxStock,
|
||||
threshold,
|
||||
const DeepCollectionEquality().hash(_metadata),
|
||||
game,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
);
|
||||
@@ -630,12 +551,7 @@ abstract class _GamePrize implements GamePrize {
|
||||
required final String id,
|
||||
required final String gameId,
|
||||
required final String name,
|
||||
required final int weight,
|
||||
required final int stock,
|
||||
required final int maxStock,
|
||||
required final int threshold,
|
||||
required final Map<String, dynamic> metadata,
|
||||
required final Game game,
|
||||
required final String createdAt,
|
||||
required final String updatedAt,
|
||||
}) = _$GamePrizeImpl;
|
||||
@@ -647,18 +563,8 @@ abstract class _GamePrize implements GamePrize {
|
||||
@override
|
||||
String get name;
|
||||
@override
|
||||
int get weight;
|
||||
@override
|
||||
int get stock;
|
||||
@override
|
||||
int get maxStock;
|
||||
@override
|
||||
int get threshold;
|
||||
@override
|
||||
Map<String, dynamic> get metadata;
|
||||
@override
|
||||
Game get game;
|
||||
@override
|
||||
String get createdAt;
|
||||
@override
|
||||
String get updatedAt;
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
part of '../game.dart';
|
||||
|
||||
abstract class IGameRepository {
|
||||
Future<Either<GameFailure, List<GamePrize>>> gamePrizeByGameId({
|
||||
required String id,
|
||||
});
|
||||
Future<Either<GameFailure, Game>> ferrisWheel();
|
||||
}
|
||||
|
||||
+3
-2
@@ -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(
|
||||
|
||||
@@ -17,12 +17,10 @@ class GameRemoteDataProvider {
|
||||
|
||||
GameRemoteDataProvider(this._apiClient);
|
||||
|
||||
Future<DC<GameFailure, List<GamePrizeDto>>> gamePrizeByGameId({
|
||||
required String id,
|
||||
}) async {
|
||||
Future<DC<GameFailure, GameDto>> ferrisWheel() async {
|
||||
try {
|
||||
final response = await _apiClient.get(
|
||||
'${ApiPath.gamePrizeByGameId}/$id',
|
||||
ApiPath.ferrisWheel,
|
||||
headers: getAuthorizationHeader(),
|
||||
);
|
||||
|
||||
@@ -38,12 +36,11 @@ class GameRemoteDataProvider {
|
||||
);
|
||||
}
|
||||
|
||||
final dto = (response.data['data'] as List)
|
||||
.map((e) => GamePrizeDto.fromJson(e))
|
||||
.toList();
|
||||
final dto = GameDto.fromJson(response.data['data']['data']['game']);
|
||||
|
||||
return DC.data(dto);
|
||||
} on ApiFailure catch (e, s) {
|
||||
log('gamePrizeByGameId', name: _logName, error: e, stackTrace: s);
|
||||
log('ferrisWheel', name: _logName, error: e, stackTrace: s);
|
||||
return DC.error(GameFailure.serverError(e));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ class GameDto with _$GameDto {
|
||||
@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;
|
||||
@@ -22,7 +23,8 @@ class GameDto with _$GameDto {
|
||||
name: name ?? '',
|
||||
type: type ?? '',
|
||||
isActive: isActive ?? false,
|
||||
metadata: metadata ?? const {},
|
||||
metadata: metadata ?? {},
|
||||
prizes: prizes?.map((e) => e.toDomain()).toList() ?? [],
|
||||
createdAt: createdAt ?? '',
|
||||
updatedAt: updatedAt ?? '',
|
||||
);
|
||||
|
||||
@@ -6,12 +6,7 @@ class GamePrizeDto with _$GamePrizeDto {
|
||||
@JsonKey(name: 'id') String? id,
|
||||
@JsonKey(name: 'game_id') String? gameId,
|
||||
@JsonKey(name: 'name') String? name,
|
||||
@JsonKey(name: 'weight') int? weight,
|
||||
@JsonKey(name: 'stock') int? stock,
|
||||
@JsonKey(name: 'max_stock') int? maxStock,
|
||||
@JsonKey(name: 'threshold') int? threshold,
|
||||
@JsonKey(name: 'metadata') Map<String, dynamic>? metadata,
|
||||
@JsonKey(name: 'game') GameDto? game,
|
||||
@JsonKey(name: 'created_at') String? createdAt,
|
||||
@JsonKey(name: 'updated_at') String? updatedAt,
|
||||
}) = _GamePrizeDto;
|
||||
@@ -21,17 +16,11 @@ class GamePrizeDto with _$GamePrizeDto {
|
||||
|
||||
const GamePrizeDto._();
|
||||
|
||||
/// mapping ke domain
|
||||
GamePrize toDomain() => GamePrize(
|
||||
id: id ?? '',
|
||||
gameId: gameId ?? '',
|
||||
name: name ?? '',
|
||||
weight: weight ?? 0,
|
||||
stock: stock ?? 0,
|
||||
maxStock: maxStock ?? 0,
|
||||
threshold: threshold ?? 0,
|
||||
metadata: metadata ?? const {},
|
||||
game: game?.toDomain() ?? Game.empty(),
|
||||
metadata: metadata ?? {},
|
||||
createdAt: createdAt ?? '',
|
||||
updatedAt: updatedAt ?? '',
|
||||
);
|
||||
|
||||
@@ -31,6 +31,8 @@ mixin _$GameDto {
|
||||
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')
|
||||
@@ -56,6 +58,7 @@ abstract class $GameDtoCopyWith<$Res> {
|
||||
@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,
|
||||
});
|
||||
@@ -81,6 +84,7 @@ class _$GameDtoCopyWithImpl<$Res, $Val extends GameDto>
|
||||
Object? type = freezed,
|
||||
Object? isActive = freezed,
|
||||
Object? metadata = freezed,
|
||||
Object? prizes = freezed,
|
||||
Object? createdAt = freezed,
|
||||
Object? updatedAt = freezed,
|
||||
}) {
|
||||
@@ -106,6 +110,10 @@ class _$GameDtoCopyWithImpl<$Res, $Val extends GameDto>
|
||||
? _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
|
||||
@@ -134,6 +142,7 @@ abstract class _$$GameDtoImplCopyWith<$Res> implements $GameDtoCopyWith<$Res> {
|
||||
@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,
|
||||
});
|
||||
@@ -158,6 +167,7 @@ class __$$GameDtoImplCopyWithImpl<$Res>
|
||||
Object? type = freezed,
|
||||
Object? isActive = freezed,
|
||||
Object? metadata = freezed,
|
||||
Object? prizes = freezed,
|
||||
Object? createdAt = freezed,
|
||||
Object? updatedAt = freezed,
|
||||
}) {
|
||||
@@ -183,6 +193,10 @@ class __$$GameDtoImplCopyWithImpl<$Res>
|
||||
? _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
|
||||
@@ -205,9 +219,11 @@ class _$GameDtoImpl extends _GameDto {
|
||||
@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) =>
|
||||
@@ -236,6 +252,17 @@ class _$GameDtoImpl extends _GameDto {
|
||||
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;
|
||||
@@ -245,7 +272,7 @@ class _$GameDtoImpl extends _GameDto {
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'GameDto(id: $id, name: $name, type: $type, isActive: $isActive, metadata: $metadata, createdAt: $createdAt, updatedAt: $updatedAt)';
|
||||
return 'GameDto(id: $id, name: $name, type: $type, isActive: $isActive, metadata: $metadata, prizes: $prizes, createdAt: $createdAt, updatedAt: $updatedAt)';
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -259,6 +286,7 @@ class _$GameDtoImpl extends _GameDto {
|
||||
(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) ||
|
||||
@@ -274,6 +302,7 @@ class _$GameDtoImpl extends _GameDto {
|
||||
type,
|
||||
isActive,
|
||||
const DeepCollectionEquality().hash(_metadata),
|
||||
const DeepCollectionEquality().hash(_prizes),
|
||||
createdAt,
|
||||
updatedAt,
|
||||
);
|
||||
@@ -299,6 +328,7 @@ abstract class _GameDto extends GameDto {
|
||||
@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;
|
||||
@@ -322,6 +352,9 @@ abstract class _GameDto extends GameDto {
|
||||
@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
|
||||
@@ -348,18 +381,8 @@ mixin _$GamePrizeDto {
|
||||
String? get gameId => throw _privateConstructorUsedError;
|
||||
@JsonKey(name: 'name')
|
||||
String? get name => throw _privateConstructorUsedError;
|
||||
@JsonKey(name: 'weight')
|
||||
int? get weight => throw _privateConstructorUsedError;
|
||||
@JsonKey(name: 'stock')
|
||||
int? get stock => throw _privateConstructorUsedError;
|
||||
@JsonKey(name: 'max_stock')
|
||||
int? get maxStock => throw _privateConstructorUsedError;
|
||||
@JsonKey(name: 'threshold')
|
||||
int? get threshold => throw _privateConstructorUsedError;
|
||||
@JsonKey(name: 'metadata')
|
||||
Map<String, dynamic>? get metadata => throw _privateConstructorUsedError;
|
||||
@JsonKey(name: 'game')
|
||||
GameDto? get game => throw _privateConstructorUsedError;
|
||||
@JsonKey(name: 'created_at')
|
||||
String? get createdAt => throw _privateConstructorUsedError;
|
||||
@JsonKey(name: 'updated_at')
|
||||
@@ -386,17 +409,10 @@ abstract class $GamePrizeDtoCopyWith<$Res> {
|
||||
@JsonKey(name: 'id') String? id,
|
||||
@JsonKey(name: 'game_id') String? gameId,
|
||||
@JsonKey(name: 'name') String? name,
|
||||
@JsonKey(name: 'weight') int? weight,
|
||||
@JsonKey(name: 'stock') int? stock,
|
||||
@JsonKey(name: 'max_stock') int? maxStock,
|
||||
@JsonKey(name: 'threshold') int? threshold,
|
||||
@JsonKey(name: 'metadata') Map<String, dynamic>? metadata,
|
||||
@JsonKey(name: 'game') GameDto? game,
|
||||
@JsonKey(name: 'created_at') String? createdAt,
|
||||
@JsonKey(name: 'updated_at') String? updatedAt,
|
||||
});
|
||||
|
||||
$GameDtoCopyWith<$Res>? get game;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@@ -417,12 +433,7 @@ class _$GamePrizeDtoCopyWithImpl<$Res, $Val extends GamePrizeDto>
|
||||
Object? id = freezed,
|
||||
Object? gameId = freezed,
|
||||
Object? name = freezed,
|
||||
Object? weight = freezed,
|
||||
Object? stock = freezed,
|
||||
Object? maxStock = freezed,
|
||||
Object? threshold = freezed,
|
||||
Object? metadata = freezed,
|
||||
Object? game = freezed,
|
||||
Object? createdAt = freezed,
|
||||
Object? updatedAt = freezed,
|
||||
}) {
|
||||
@@ -440,30 +451,10 @@ class _$GamePrizeDtoCopyWithImpl<$Res, $Val extends GamePrizeDto>
|
||||
? _value.name
|
||||
: name // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
weight: freezed == weight
|
||||
? _value.weight
|
||||
: weight // ignore: cast_nullable_to_non_nullable
|
||||
as int?,
|
||||
stock: freezed == stock
|
||||
? _value.stock
|
||||
: stock // ignore: cast_nullable_to_non_nullable
|
||||
as int?,
|
||||
maxStock: freezed == maxStock
|
||||
? _value.maxStock
|
||||
: maxStock // ignore: cast_nullable_to_non_nullable
|
||||
as int?,
|
||||
threshold: freezed == threshold
|
||||
? _value.threshold
|
||||
: threshold // ignore: cast_nullable_to_non_nullable
|
||||
as int?,
|
||||
metadata: freezed == metadata
|
||||
? _value.metadata
|
||||
: metadata // ignore: cast_nullable_to_non_nullable
|
||||
as Map<String, dynamic>?,
|
||||
game: freezed == game
|
||||
? _value.game
|
||||
: game // ignore: cast_nullable_to_non_nullable
|
||||
as GameDto?,
|
||||
createdAt: freezed == createdAt
|
||||
? _value.createdAt
|
||||
: createdAt // ignore: cast_nullable_to_non_nullable
|
||||
@@ -476,20 +467,6 @@ class _$GamePrizeDtoCopyWithImpl<$Res, $Val extends GamePrizeDto>
|
||||
as $Val,
|
||||
);
|
||||
}
|
||||
|
||||
/// Create a copy of GamePrizeDto
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$GameDtoCopyWith<$Res>? get game {
|
||||
if (_value.game == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $GameDtoCopyWith<$Res>(_value.game!, (value) {
|
||||
return _then(_value.copyWith(game: value) as $Val);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@@ -505,18 +482,10 @@ abstract class _$$GamePrizeDtoImplCopyWith<$Res>
|
||||
@JsonKey(name: 'id') String? id,
|
||||
@JsonKey(name: 'game_id') String? gameId,
|
||||
@JsonKey(name: 'name') String? name,
|
||||
@JsonKey(name: 'weight') int? weight,
|
||||
@JsonKey(name: 'stock') int? stock,
|
||||
@JsonKey(name: 'max_stock') int? maxStock,
|
||||
@JsonKey(name: 'threshold') int? threshold,
|
||||
@JsonKey(name: 'metadata') Map<String, dynamic>? metadata,
|
||||
@JsonKey(name: 'game') GameDto? game,
|
||||
@JsonKey(name: 'created_at') String? createdAt,
|
||||
@JsonKey(name: 'updated_at') String? updatedAt,
|
||||
});
|
||||
|
||||
@override
|
||||
$GameDtoCopyWith<$Res>? get game;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@@ -536,12 +505,7 @@ class __$$GamePrizeDtoImplCopyWithImpl<$Res>
|
||||
Object? id = freezed,
|
||||
Object? gameId = freezed,
|
||||
Object? name = freezed,
|
||||
Object? weight = freezed,
|
||||
Object? stock = freezed,
|
||||
Object? maxStock = freezed,
|
||||
Object? threshold = freezed,
|
||||
Object? metadata = freezed,
|
||||
Object? game = freezed,
|
||||
Object? createdAt = freezed,
|
||||
Object? updatedAt = freezed,
|
||||
}) {
|
||||
@@ -559,30 +523,10 @@ class __$$GamePrizeDtoImplCopyWithImpl<$Res>
|
||||
? _value.name
|
||||
: name // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
weight: freezed == weight
|
||||
? _value.weight
|
||||
: weight // ignore: cast_nullable_to_non_nullable
|
||||
as int?,
|
||||
stock: freezed == stock
|
||||
? _value.stock
|
||||
: stock // ignore: cast_nullable_to_non_nullable
|
||||
as int?,
|
||||
maxStock: freezed == maxStock
|
||||
? _value.maxStock
|
||||
: maxStock // ignore: cast_nullable_to_non_nullable
|
||||
as int?,
|
||||
threshold: freezed == threshold
|
||||
? _value.threshold
|
||||
: threshold // ignore: cast_nullable_to_non_nullable
|
||||
as int?,
|
||||
metadata: freezed == metadata
|
||||
? _value._metadata
|
||||
: metadata // ignore: cast_nullable_to_non_nullable
|
||||
as Map<String, dynamic>?,
|
||||
game: freezed == game
|
||||
? _value.game
|
||||
: game // ignore: cast_nullable_to_non_nullable
|
||||
as GameDto?,
|
||||
createdAt: freezed == createdAt
|
||||
? _value.createdAt
|
||||
: createdAt // ignore: cast_nullable_to_non_nullable
|
||||
@@ -603,12 +547,7 @@ class _$GamePrizeDtoImpl extends _GamePrizeDto {
|
||||
@JsonKey(name: 'id') this.id,
|
||||
@JsonKey(name: 'game_id') this.gameId,
|
||||
@JsonKey(name: 'name') this.name,
|
||||
@JsonKey(name: 'weight') this.weight,
|
||||
@JsonKey(name: 'stock') this.stock,
|
||||
@JsonKey(name: 'max_stock') this.maxStock,
|
||||
@JsonKey(name: 'threshold') this.threshold,
|
||||
@JsonKey(name: 'metadata') final Map<String, dynamic>? metadata,
|
||||
@JsonKey(name: 'game') this.game,
|
||||
@JsonKey(name: 'created_at') this.createdAt,
|
||||
@JsonKey(name: 'updated_at') this.updatedAt,
|
||||
}) : _metadata = metadata,
|
||||
@@ -626,18 +565,6 @@ class _$GamePrizeDtoImpl extends _GamePrizeDto {
|
||||
@override
|
||||
@JsonKey(name: 'name')
|
||||
final String? name;
|
||||
@override
|
||||
@JsonKey(name: 'weight')
|
||||
final int? weight;
|
||||
@override
|
||||
@JsonKey(name: 'stock')
|
||||
final int? stock;
|
||||
@override
|
||||
@JsonKey(name: 'max_stock')
|
||||
final int? maxStock;
|
||||
@override
|
||||
@JsonKey(name: 'threshold')
|
||||
final int? threshold;
|
||||
final Map<String, dynamic>? _metadata;
|
||||
@override
|
||||
@JsonKey(name: 'metadata')
|
||||
@@ -649,9 +576,6 @@ class _$GamePrizeDtoImpl extends _GamePrizeDto {
|
||||
return EqualUnmodifiableMapView(value);
|
||||
}
|
||||
|
||||
@override
|
||||
@JsonKey(name: 'game')
|
||||
final GameDto? game;
|
||||
@override
|
||||
@JsonKey(name: 'created_at')
|
||||
final String? createdAt;
|
||||
@@ -661,7 +585,7 @@ class _$GamePrizeDtoImpl extends _GamePrizeDto {
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'GamePrizeDto(id: $id, gameId: $gameId, name: $name, weight: $weight, stock: $stock, maxStock: $maxStock, threshold: $threshold, metadata: $metadata, game: $game, createdAt: $createdAt, updatedAt: $updatedAt)';
|
||||
return 'GamePrizeDto(id: $id, gameId: $gameId, name: $name, metadata: $metadata, createdAt: $createdAt, updatedAt: $updatedAt)';
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -672,14 +596,7 @@ class _$GamePrizeDtoImpl extends _GamePrizeDto {
|
||||
(identical(other.id, id) || other.id == id) &&
|
||||
(identical(other.gameId, gameId) || other.gameId == gameId) &&
|
||||
(identical(other.name, name) || other.name == name) &&
|
||||
(identical(other.weight, weight) || other.weight == weight) &&
|
||||
(identical(other.stock, stock) || other.stock == stock) &&
|
||||
(identical(other.maxStock, maxStock) ||
|
||||
other.maxStock == maxStock) &&
|
||||
(identical(other.threshold, threshold) ||
|
||||
other.threshold == threshold) &&
|
||||
const DeepCollectionEquality().equals(other._metadata, _metadata) &&
|
||||
(identical(other.game, game) || other.game == game) &&
|
||||
(identical(other.createdAt, createdAt) ||
|
||||
other.createdAt == createdAt) &&
|
||||
(identical(other.updatedAt, updatedAt) ||
|
||||
@@ -693,12 +610,7 @@ class _$GamePrizeDtoImpl extends _GamePrizeDto {
|
||||
id,
|
||||
gameId,
|
||||
name,
|
||||
weight,
|
||||
stock,
|
||||
maxStock,
|
||||
threshold,
|
||||
const DeepCollectionEquality().hash(_metadata),
|
||||
game,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
);
|
||||
@@ -722,12 +634,7 @@ abstract class _GamePrizeDto extends GamePrizeDto {
|
||||
@JsonKey(name: 'id') final String? id,
|
||||
@JsonKey(name: 'game_id') final String? gameId,
|
||||
@JsonKey(name: 'name') final String? name,
|
||||
@JsonKey(name: 'weight') final int? weight,
|
||||
@JsonKey(name: 'stock') final int? stock,
|
||||
@JsonKey(name: 'max_stock') final int? maxStock,
|
||||
@JsonKey(name: 'threshold') final int? threshold,
|
||||
@JsonKey(name: 'metadata') final Map<String, dynamic>? metadata,
|
||||
@JsonKey(name: 'game') final GameDto? game,
|
||||
@JsonKey(name: 'created_at') final String? createdAt,
|
||||
@JsonKey(name: 'updated_at') final String? updatedAt,
|
||||
}) = _$GamePrizeDtoImpl;
|
||||
@@ -746,24 +653,9 @@ abstract class _GamePrizeDto extends GamePrizeDto {
|
||||
@JsonKey(name: 'name')
|
||||
String? get name;
|
||||
@override
|
||||
@JsonKey(name: 'weight')
|
||||
int? get weight;
|
||||
@override
|
||||
@JsonKey(name: 'stock')
|
||||
int? get stock;
|
||||
@override
|
||||
@JsonKey(name: 'max_stock')
|
||||
int? get maxStock;
|
||||
@override
|
||||
@JsonKey(name: 'threshold')
|
||||
int? get threshold;
|
||||
@override
|
||||
@JsonKey(name: 'metadata')
|
||||
Map<String, dynamic>? get metadata;
|
||||
@override
|
||||
@JsonKey(name: 'game')
|
||||
GameDto? get game;
|
||||
@override
|
||||
@JsonKey(name: 'created_at')
|
||||
String? get createdAt;
|
||||
@override
|
||||
|
||||
@@ -13,6 +13,9 @@ _$GameDtoImpl _$$GameDtoImplFromJson(Map<String, dynamic> json) =>
|
||||
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?,
|
||||
);
|
||||
@@ -24,6 +27,7 @@ Map<String, dynamic> _$$GameDtoImplToJson(_$GameDtoImpl instance) =>
|
||||
'type': instance.type,
|
||||
'is_active': instance.isActive,
|
||||
'metadata': instance.metadata,
|
||||
'prizes': instance.prizes,
|
||||
'created_at': instance.createdAt,
|
||||
'updated_at': instance.updatedAt,
|
||||
};
|
||||
@@ -33,14 +37,7 @@ _$GamePrizeDtoImpl _$$GamePrizeDtoImplFromJson(Map<String, dynamic> json) =>
|
||||
id: json['id'] as String?,
|
||||
gameId: json['game_id'] as String?,
|
||||
name: json['name'] as String?,
|
||||
weight: (json['weight'] as num?)?.toInt(),
|
||||
stock: (json['stock'] as num?)?.toInt(),
|
||||
maxStock: (json['max_stock'] as num?)?.toInt(),
|
||||
threshold: (json['threshold'] as num?)?.toInt(),
|
||||
metadata: json['metadata'] as Map<String, dynamic>?,
|
||||
game: json['game'] == null
|
||||
? null
|
||||
: GameDto.fromJson(json['game'] as Map<String, dynamic>),
|
||||
createdAt: json['created_at'] as String?,
|
||||
updatedAt: json['updated_at'] as String?,
|
||||
);
|
||||
@@ -50,12 +47,7 @@ Map<String, dynamic> _$$GamePrizeDtoImplToJson(_$GamePrizeDtoImpl instance) =>
|
||||
'id': instance.id,
|
||||
'game_id': instance.gameId,
|
||||
'name': instance.name,
|
||||
'weight': instance.weight,
|
||||
'stock': instance.stock,
|
||||
'max_stock': instance.maxStock,
|
||||
'threshold': instance.threshold,
|
||||
'metadata': instance.metadata,
|
||||
'game': instance.game,
|
||||
'created_at': instance.createdAt,
|
||||
'updated_at': instance.updatedAt,
|
||||
};
|
||||
|
||||
@@ -15,21 +15,19 @@ class GameRepository implements IGameRepository {
|
||||
GameRepository(this._remoteDataProvider);
|
||||
|
||||
@override
|
||||
Future<Either<GameFailure, List<GamePrize>>> gamePrizeByGameId({
|
||||
required String id,
|
||||
}) async {
|
||||
Future<Either<GameFailure, Game>> ferrisWheel() async {
|
||||
try {
|
||||
final result = await _remoteDataProvider.gamePrizeByGameId(id: id);
|
||||
final result = await _remoteDataProvider.ferrisWheel();
|
||||
|
||||
if (result.hasError) {
|
||||
return left(result.error!);
|
||||
}
|
||||
|
||||
final data = result.data!.map((e) => e.toDomain()).toList();
|
||||
final data = result.data!.toDomain();
|
||||
|
||||
return right(data);
|
||||
} catch (e, s) {
|
||||
log('gamePrizeByGameId', name: _logName, error: e, stackTrace: s);
|
||||
log('ferrisWheel', name: _logName, error: e, stackTrace: s);
|
||||
return left(const GameFailure.unexpectedError());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,8 +28,8 @@ 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/game_price_loader/game_prize_loader_bloc.dart'
|
||||
as _i925;
|
||||
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;
|
||||
@@ -103,8 +103,8 @@ extension GetItInjectableX on _i174.GetIt {
|
||||
gh.factory<_i96.IGameRepository>(
|
||||
() => _i547.GameRepository(gh<_i143.GameRemoteDataProvider>()),
|
||||
);
|
||||
gh.factory<_i925.GamePrizeLoaderBloc>(
|
||||
() => _i925.GamePrizeLoaderBloc(gh<_i96.IGameRepository>()),
|
||||
gh.factory<_i1013.FerrisWheelLoaderBloc>(
|
||||
() => _i1013.FerrisWheelLoaderBloc(gh<_i96.IGameRepository>()),
|
||||
);
|
||||
gh.factory<_i995.IAuthRepository>(
|
||||
() => _i879.AuthRepository(
|
||||
@@ -124,12 +124,12 @@ 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>()),
|
||||
);
|
||||
|
||||
@@ -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)]),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
+5
-5
@@ -60,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
|
||||
@@ -276,7 +276,7 @@ class _PoinPageState extends State<PoinPage> {
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: Icon(Icons.history),
|
||||
onPressed: () => context.router.push(PoinHistoryRoute()),
|
||||
onPressed: () => context.router.push(CoinHistoryRoute()),
|
||||
),
|
||||
],
|
||||
),
|
||||
+4
-4
@@ -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
|
||||
+1
-1
@@ -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 {
|
||||
@@ -24,7 +24,12 @@ class _MainPageState extends State<MainPage> {
|
||||
@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,
|
||||
|
||||
@@ -5,13 +5,14 @@ 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 {
|
||||
@@ -52,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(),
|
||||
],
|
||||
),
|
||||
|
||||
+5
-6
@@ -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,52 +1,60 @@
|
||||
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 '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 BlocBuilder<AuthBloc, AuthState>(
|
||||
builder: (context, state) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
HomeFeatureCard(
|
||||
icon: Icons.card_giftcard,
|
||||
title: 'Reward',
|
||||
iconColor: const Color(0xFF1976D2),
|
||||
onTap: () => context.router.push(RewardRoute()),
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
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: () => state.isAuthenticated
|
||||
? context.router.push(FerrisWheelRoute())
|
||||
: context.router.push(OnboardingRoute()),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ class HomePointCard extends StatelessWidget {
|
||||
builder: (context, state) {
|
||||
return GestureDetector(
|
||||
onTap: () => state.isAuthenticated
|
||||
? context.router.push(PoinRoute())
|
||||
? context.router.push(CoinRoute())
|
||||
: context.router.push(OnboardingRoute()),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ 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';
|
||||
|
||||
@@ -46,165 +47,81 @@ class ProfilePage extends StatelessWidget implements AutoRouteWrapper {
|
||||
onTap: () => state.isAuthenticated
|
||||
? context.router.push(AccountMyRoute())
|
||||
: context.router.push(OnboardingRoute()),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: AppColor.primaryGradient,
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Stack(
|
||||
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),
|
||||
),
|
||||
),
|
||||
),
|
||||
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),
|
||||
child: GradientCard(
|
||||
child: !state.isAuthenticated
|
||||
? Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Silahkan Masuk',
|
||||
style: AppStyle.lg.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColor.white,
|
||||
letterSpacing: 0.5,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
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: !state.isAuthenticated
|
||||
? Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Silahkan Masuk',
|
||||
style: AppStyle.lg.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColor.white,
|
||||
letterSpacing: 0.5,
|
||||
),
|
||||
],
|
||||
)
|
||||
: Row(
|
||||
children: [
|
||||
// Avatar
|
||||
Container(
|
||||
width: 60,
|
||||
height: 60,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.white,
|
||||
shape: BoxShape.circle,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: AppColor.black.withOpacity(
|
||||
0.1,
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
: Row(
|
||||
children: [
|
||||
// Avatar
|
||||
Container(
|
||||
width: 60,
|
||||
height: 60,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.white,
|
||||
shape: BoxShape.circle,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: AppColor.black
|
||||
.withOpacity(0.1),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Icon(
|
||||
Icons.person,
|
||||
size: 30,
|
||||
color: AppColor.primary,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
// User Info
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
state.user.name,
|
||||
style: AppStyle.lg.copyWith(
|
||||
fontWeight:
|
||||
FontWeight.bold,
|
||||
color: AppColor.white,
|
||||
letterSpacing: 0.5,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
state.user.phoneNumber,
|
||||
style: AppStyle.sm.copyWith(
|
||||
color: AppColor.white
|
||||
.withOpacity(0.9),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// Arrow Icon
|
||||
Icon(
|
||||
Icons.arrow_forward_ios,
|
||||
color: AppColor.white,
|
||||
size: 14,
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Icon(
|
||||
Icons.person,
|
||||
size: 30,
|
||||
color: AppColor.primary,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
// User Info
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
state.user.name,
|
||||
style: AppStyle.lg.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColor.white,
|
||||
letterSpacing: 0.5,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
state.user.phoneNumber,
|
||||
style: AppStyle.sm.copyWith(
|
||||
color: AppColor.white
|
||||
.withOpacity(0.9),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// Arrow Icon
|
||||
Icon(
|
||||
Icons.arrow_forward_ios,
|
||||
color: AppColor.white,
|
||||
size: 14,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -6,12 +6,30 @@ import 'package:flutter_spinkit/flutter_spinkit.dart';
|
||||
import 'dart:math' as math;
|
||||
|
||||
import '../../../../application/auth/auth_bloc.dart';
|
||||
import '../../../../application/game/game_price_loader/game_prize_loader_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 '../../../../injection.dart';
|
||||
import '../../../../domain/game/game.dart';
|
||||
import 'data/model.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 implements AutoRouteWrapper {
|
||||
@@ -22,12 +40,9 @@ class FerrisWheelPage extends StatefulWidget implements AutoRouteWrapper {
|
||||
|
||||
@override
|
||||
Widget wrappedRoute(BuildContext context) => BlocProvider(
|
||||
create: (context) => getIt<GamePrizeLoaderBloc>()
|
||||
..add(
|
||||
const GamePrizeLoaderEvent.fetched(
|
||||
'28d5aed3-4c1b-4b7b-bad5-67cdd4919dc2',
|
||||
),
|
||||
),
|
||||
create: (context) =>
|
||||
getIt<FerrisWheelLoaderBloc>()
|
||||
..add(const FerrisWheelLoaderEvent.fetched()),
|
||||
child: this,
|
||||
);
|
||||
}
|
||||
@@ -84,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');
|
||||
}
|
||||
@@ -107,23 +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(GamePrize prize) {
|
||||
int prizeValue = prize.metadata['value'] ?? prize.weight;
|
||||
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);
|
||||
@@ -170,66 +170,48 @@ class _FerrisWheelPageState extends State<FerrisWheelPage>
|
||||
int _selectPrizeWithWeight() {
|
||||
if (gamePrizes.isEmpty) return 0;
|
||||
|
||||
final availablePrizes = gamePrizes
|
||||
.asMap()
|
||||
.entries
|
||||
.where((entry) => entry.value.stock > 0)
|
||||
.toList();
|
||||
// Jika GamePrize memiliki properti weight, gunakan ini:
|
||||
// List<int> weights = gamePrizes.map((prize) => prize.weight ?? 1).toList();
|
||||
|
||||
if (availablePrizes.isEmpty) {
|
||||
return math.Random().nextInt(gamePrizes.length);
|
||||
// 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);
|
||||
}
|
||||
|
||||
int totalWeight = availablePrizes
|
||||
.map((entry) => entry.value.weight)
|
||||
.reduce((a, b) => a + b);
|
||||
// Hitung total bobot
|
||||
int totalWeight = weights.reduce((a, b) => a + b);
|
||||
|
||||
if (totalWeight <= 0) {
|
||||
final randomEntry =
|
||||
availablePrizes[math.Random().nextInt(availablePrizes.length)];
|
||||
return randomEntry.key;
|
||||
}
|
||||
// Generate random number
|
||||
int randomNum = math.Random().nextInt(totalWeight);
|
||||
|
||||
int randomWeight = math.Random().nextInt(totalWeight);
|
||||
// Tentukan hadiah berdasarkan bobot
|
||||
int currentWeight = 0;
|
||||
|
||||
for (final entry in availablePrizes) {
|
||||
currentWeight += entry.value.weight;
|
||||
if (randomWeight < currentWeight) {
|
||||
return entry.key;
|
||||
for (int i = 0; i < gamePrizes.length; i++) {
|
||||
currentWeight += weights[i];
|
||||
if (randomNum < currentWeight) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
return availablePrizes.last.key;
|
||||
// Fallback (seharusnya tidak pernah terjadi)
|
||||
return gamePrizes.length - 1;
|
||||
}
|
||||
|
||||
Color _getPrizeColor(GamePrize prize, int index) {
|
||||
final colorName = prize.metadata['color'] as String?;
|
||||
switch (colorName?.toLowerCase()) {
|
||||
case 'primary':
|
||||
return AppColor.primary;
|
||||
case 'info':
|
||||
return AppColor.info;
|
||||
case 'warning':
|
||||
return AppColor.warning;
|
||||
case 'success':
|
||||
return AppColor.success;
|
||||
case 'error':
|
||||
return AppColor.error;
|
||||
case 'secondary':
|
||||
return AppColor.secondary;
|
||||
default:
|
||||
final colors = [
|
||||
AppColor.primary,
|
||||
AppColor.info,
|
||||
AppColor.warning,
|
||||
AppColor.success,
|
||||
AppColor.primaryDark,
|
||||
AppColor.secondary,
|
||||
AppColor.error,
|
||||
];
|
||||
return colors[index % colors.length];
|
||||
}
|
||||
final colors = [
|
||||
AppColor.primary,
|
||||
AppColor.info,
|
||||
AppColor.warning,
|
||||
AppColor.success,
|
||||
AppColor.primaryDark,
|
||||
AppColor.secondary,
|
||||
AppColor.error,
|
||||
];
|
||||
return colors[index % colors.length];
|
||||
}
|
||||
|
||||
void _spinWheel() {
|
||||
@@ -245,23 +227,25 @@ class _FerrisWheelPageState extends State<FerrisWheelPage>
|
||||
});
|
||||
|
||||
_idleRotationController.stop();
|
||||
_idleRotationController.reset();
|
||||
|
||||
int targetSection = _selectPrizeWithWeight();
|
||||
int selectedPrizeIndex = _selectPrizeWithWeight();
|
||||
double sectionAngle = (2 * math.pi) / gamePrizes.length;
|
||||
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;
|
||||
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,
|
||||
@@ -270,27 +254,20 @@ class _FerrisWheelPageState extends State<FerrisWheelPage>
|
||||
|
||||
_rotationController.reset();
|
||||
_rotationController.animateTo(1.0).then((_) {
|
||||
final wonPrize = gamePrizes[targetSection];
|
||||
_playWinSound(wonPrize);
|
||||
final wonPrize = gamePrizes[selectedPrizeIndex];
|
||||
_playWinSound();
|
||||
|
||||
setState(() {
|
||||
currentRotation = finalRotation;
|
||||
isSpinning = false;
|
||||
resultText = 'Selamat! Anda mendapat ${wonPrize.name}!';
|
||||
|
||||
if (wonPrize.stock > 0) {
|
||||
gamePrizes[targetSection] = wonPrize.copyWith(
|
||||
stock: wonPrize.stock - 1,
|
||||
);
|
||||
}
|
||||
|
||||
prizeHistory.insert(
|
||||
0,
|
||||
PrizeHistory(
|
||||
prize: wonPrize.name,
|
||||
dateTime: DateTime.now(),
|
||||
value: wonPrize.metadata['value'] ?? wonPrize.weight,
|
||||
color: _getPrizeColor(wonPrize, targetSection),
|
||||
value: 100,
|
||||
color: _getPrizeColor(wonPrize, selectedPrizeIndex),
|
||||
icon: Icons.card_giftcard,
|
||||
gamePrizeId: wonPrize.id,
|
||||
),
|
||||
@@ -298,7 +275,6 @@ class _FerrisWheelPageState extends State<FerrisWheelPage>
|
||||
});
|
||||
|
||||
_showWinDialog(wonPrize);
|
||||
_idleRotationController.reset();
|
||||
_idleRotationController.repeat();
|
||||
});
|
||||
}
|
||||
@@ -363,15 +339,6 @@ class _FerrisWheelPageState extends State<FerrisWheelPage>
|
||||
color: AppColor.textWhite,
|
||||
),
|
||||
),
|
||||
if (wonPrize.stock >= 0) ...[
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Stok tersisa: ${wonPrize.stock}',
|
||||
style: AppStyle.sm.copyWith(
|
||||
color: AppColor.textWhite.withOpacity(0.8),
|
||||
),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 20),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
@@ -406,7 +373,7 @@ class _FerrisWheelPageState extends State<FerrisWheelPage>
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<GamePrizeLoaderBloc, GamePrizeLoaderState>(
|
||||
return BlocBuilder<FerrisWheelLoaderBloc, FerrisWheelLoaderState>(
|
||||
builder: (context, state) {
|
||||
if (state.isFetching) {
|
||||
return Scaffold(
|
||||
@@ -425,10 +392,10 @@ class _FerrisWheelPageState extends State<FerrisWheelPage>
|
||||
);
|
||||
}
|
||||
|
||||
if (gamePrizes.isEmpty && state.gamePrize.isNotEmpty) {
|
||||
if (gamePrizes.isEmpty && state.ferrisWheel.prizes.isNotEmpty) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
setState(() {
|
||||
gamePrizes = state.gamePrize;
|
||||
gamePrizes = state.ferrisWheel.prizes;
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -445,100 +412,9 @@ class _FerrisWheelPageState extends State<FerrisWheelPage>
|
||||
child: SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
// Header
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Row(
|
||||
children: [
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
_playButtonTap();
|
||||
context.router.back();
|
||||
},
|
||||
icon: Icon(
|
||||
Icons.close,
|
||||
color: AppColor.textWhite,
|
||||
size: 28,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'SPIN & WIN',
|
||||
style: AppStyle.h6.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColor.textWhite,
|
||||
letterSpacing: 2,
|
||||
),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: _toggleMusic,
|
||||
icon: Icon(
|
||||
_isMusicEnabled
|
||||
? Icons.volume_up
|
||||
: Icons.volume_off,
|
||||
color: AppColor.textWhite,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: _toggleSound,
|
||||
icon: Icon(
|
||||
_isSoundEnabled
|
||||
? Icons.graphic_eq
|
||||
: Icons.volume_mute,
|
||||
color: AppColor.textWhite,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Tab Selector
|
||||
Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 20),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.white.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(25),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
for (int i = 0; i < 3; i++)
|
||||
Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
_playButtonTap();
|
||||
setState(() => currentTabIndex = i);
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 12,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: currentTabIndex == i
|
||||
? AppColor.white
|
||||
: Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(25),
|
||||
),
|
||||
child: Text(
|
||||
['Spin Wheel', 'Daftar Hadiah', 'Riwayat'][i],
|
||||
textAlign: TextAlign.center,
|
||||
style: AppStyle.md.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: currentTabIndex == i
|
||||
? AppColor.primary
|
||||
: AppColor.textWhite,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
_buildHeader(),
|
||||
_buildTabSelector(),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Content Area
|
||||
Expanded(
|
||||
child: currentTabIndex == 0
|
||||
? _buildMainContent()
|
||||
@@ -555,254 +431,256 @@ class _FerrisWheelPageState extends State<FerrisWheelPage>
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeader() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Row(
|
||||
children: [
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
_playButtonTap();
|
||||
context.router.back();
|
||||
},
|
||||
icon: Icon(Icons.close, color: AppColor.textWhite, size: 28),
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'SPIN & WIN',
|
||||
style: AppStyle.h6.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColor.textWhite,
|
||||
letterSpacing: 2,
|
||||
),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: _toggleMusic,
|
||||
icon: Icon(
|
||||
_isMusicEnabled ? Icons.volume_up : Icons.volume_off,
|
||||
color: AppColor.textWhite,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: _toggleSound,
|
||||
icon: Icon(
|
||||
_isSoundEnabled ? Icons.graphic_eq : Icons.volume_mute,
|
||||
color: AppColor.textWhite,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTabSelector() {
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 20),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.white.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(25),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
for (int i = 0; i < 3; i++)
|
||||
Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
_playButtonTap();
|
||||
setState(() => currentTabIndex = i);
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: currentTabIndex == i
|
||||
? AppColor.white
|
||||
: Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(25),
|
||||
),
|
||||
child: Text(
|
||||
['Spin Wheel', 'Daftar Hadiah', 'Riwayat'][i],
|
||||
textAlign: TextAlign.center,
|
||||
style: AppStyle.md.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: currentTabIndex == i
|
||||
? AppColor.primary
|
||||
: AppColor.textWhite,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMainContent() {
|
||||
return Column(
|
||||
children: [
|
||||
// User Info Card
|
||||
Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.white,
|
||||
borderRadius: BorderRadius.circular(15),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: AppColor.black.withOpacity(0.1),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
BlocBuilder<AuthBloc, AuthState>(
|
||||
builder: (context, auth) {
|
||||
return Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 40,
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.primary,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
'G',
|
||||
style: AppStyle.lg.copyWith(
|
||||
color: AppColor.textWhite,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
auth.user.name,
|
||||
style: AppStyle.lg.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColor.textPrimary,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
auth.user.phoneNumber,
|
||||
style: AppStyle.sm.copyWith(
|
||||
color: AppColor.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 20,
|
||||
height: 20,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.warning,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(
|
||||
Icons.circle,
|
||||
size: 12,
|
||||
color: AppColor.textWhite,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'Token: $tokens',
|
||||
style: AppStyle.lg.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColor.textPrimary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
_buildUserInfoCard(),
|
||||
Text(
|
||||
resultText,
|
||||
style: AppStyle.md.copyWith(color: AppColor.textWhite),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Wheel Section
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: gamePrizes.isEmpty
|
||||
? SpinKitFadingCircle(color: AppColor.textWhite, size: 36)
|
||||
: Stack(
|
||||
alignment: Alignment.center,
|
||||
: _buildWheelSection(),
|
||||
),
|
||||
),
|
||||
_buildBottomBanner(),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildUserInfoCard() {
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.white,
|
||||
borderRadius: BorderRadius.circular(15),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: AppColor.black.withOpacity(0.1),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
BlocBuilder<AuthBloc, AuthState>(
|
||||
builder: (context, auth) {
|
||||
return Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 40,
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.primary,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
'G',
|
||||
style: AppStyle.lg.copyWith(
|
||||
color: AppColor.textWhite,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Glow Effect
|
||||
AnimatedBuilder(
|
||||
animation: _glowController,
|
||||
builder: (context, child) => Container(
|
||||
width: 340,
|
||||
height: 340,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: AppColor.white.withOpacity(
|
||||
0.3 + 0.2 * _glowController.value,
|
||||
),
|
||||
blurRadius: 40,
|
||||
spreadRadius: 10,
|
||||
),
|
||||
],
|
||||
),
|
||||
Text(
|
||||
auth.user.name,
|
||||
style: AppStyle.lg.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColor.textPrimary,
|
||||
),
|
||||
),
|
||||
|
||||
// Spinning Wheel
|
||||
AnimatedBuilder(
|
||||
animation: isSpinning
|
||||
? _rotationController
|
||||
: _idleRotationController,
|
||||
builder: (context, child) {
|
||||
double rotationAngle = isSpinning
|
||||
? (_spinAnimation?.value ?? currentRotation)
|
||||
: (currentRotation +
|
||||
(_idleRotationAnimation?.value ?? 0.0));
|
||||
|
||||
return Transform.rotate(
|
||||
angle: rotationAngle,
|
||||
child: CustomPaint(
|
||||
size: const Size(320, 320),
|
||||
painter: WheelPainter(
|
||||
gamePrizes: gamePrizes,
|
||||
getPrizeColor: _getPrizeColor,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
// Spin Button
|
||||
AnimatedBuilder(
|
||||
animation:
|
||||
_pulseAnimation ??
|
||||
const AlwaysStoppedAnimation(1.0),
|
||||
builder: (context, child) => Transform.scale(
|
||||
scale: _pulseAnimation?.value ?? 1.0,
|
||||
child: Container(
|
||||
width: 100,
|
||||
height: 100,
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [AppColor.warning, AppColor.warning],
|
||||
),
|
||||
shape: BoxShape.circle,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: AppColor.black.withOpacity(0.3),
|
||||
blurRadius: 15,
|
||||
offset: const Offset(0, 6),
|
||||
),
|
||||
BoxShadow(
|
||||
color: AppColor.warning.withOpacity(0.5),
|
||||
blurRadius: 20,
|
||||
spreadRadius:
|
||||
(_pulseAnimation?.value ?? 1.0) * 5,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(50),
|
||||
onTap: _spinWheel,
|
||||
child: Center(
|
||||
child: Text(
|
||||
'SPIN',
|
||||
style: AppStyle.lg.copyWith(
|
||||
color: AppColor.textWhite,
|
||||
fontWeight: FontWeight.bold,
|
||||
letterSpacing: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// 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,
|
||||
),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
auth.user.phoneNumber,
|
||||
style: AppStyle.sm.copyWith(
|
||||
color: AppColor.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 20,
|
||||
height: 20,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.warning,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(Icons.circle, size: 12, color: AppColor.textWhite),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'Token: $tokens',
|
||||
style: AppStyle.lg.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColor.textPrimary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildWheelSection() {
|
||||
return Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
// Glow Effect
|
||||
AnimatedBuilder(
|
||||
animation: _glowController,
|
||||
builder: (context, child) => Container(
|
||||
width: 340,
|
||||
height: 340,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: AppColor.white.withOpacity(
|
||||
0.3 + 0.2 * _glowController.value,
|
||||
),
|
||||
blurRadius: 40,
|
||||
spreadRadius: 10,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
// Spinning Wheel
|
||||
AnimatedBuilder(
|
||||
animation: isSpinning ? _rotationController : _idleRotationController,
|
||||
builder: (context, child) {
|
||||
double rotationAngle = isSpinning
|
||||
? (_spinAnimation?.value ?? currentRotation)
|
||||
: (currentRotation + (_idleRotationAnimation?.value ?? 0.0));
|
||||
|
||||
// Bottom Banner
|
||||
Container(
|
||||
margin: const EdgeInsets.all(20),
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [AppColor.warning, AppColor.warning],
|
||||
),
|
||||
borderRadius: BorderRadius.circular(15),
|
||||
),
|
||||
child: Text(
|
||||
'Spin 30x lagi buat mainin spesial spin',
|
||||
textAlign: TextAlign.center,
|
||||
style: AppStyle.lg.copyWith(
|
||||
color: AppColor.textWhite,
|
||||
fontWeight: FontWeight.bold,
|
||||
return Transform.rotate(
|
||||
angle: rotationAngle,
|
||||
child: CustomPaint(
|
||||
size: const Size(320, 320),
|
||||
painter: WheelPainter(
|
||||
gamePrizes: gamePrizes,
|
||||
getPrizeColor: _getPrizeColor,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
// Spin Button
|
||||
_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),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -810,6 +688,73 @@ class _FerrisWheelPageState extends State<FerrisWheelPage>
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSpinButton() {
|
||||
return AnimatedBuilder(
|
||||
animation: _pulseAnimation ?? const AlwaysStoppedAnimation(1.0),
|
||||
builder: (context, child) => Transform.scale(
|
||||
scale: _pulseAnimation?.value ?? 1.0,
|
||||
child: Container(
|
||||
width: 100,
|
||||
height: 100,
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [AppColor.warning, AppColor.warning],
|
||||
),
|
||||
shape: BoxShape.circle,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: AppColor.black.withOpacity(0.3),
|
||||
blurRadius: 15,
|
||||
offset: const Offset(0, 6),
|
||||
),
|
||||
BoxShadow(
|
||||
color: AppColor.warning.withOpacity(0.5),
|
||||
blurRadius: 20,
|
||||
spreadRadius: (_pulseAnimation?.value ?? 1.0) * 5,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(50),
|
||||
onTap: _spinWheel,
|
||||
child: Center(
|
||||
child: Text(
|
||||
'SPIN',
|
||||
style: AppStyle.lg.copyWith(
|
||||
color: AppColor.textWhite,
|
||||
fontWeight: FontWeight.bold,
|
||||
letterSpacing: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBottomBanner() {
|
||||
return Container(
|
||||
margin: const EdgeInsets.all(20),
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(colors: [AppColor.warning, AppColor.warning]),
|
||||
borderRadius: BorderRadius.circular(15),
|
||||
),
|
||||
child: Text(
|
||||
'Spin 30x lagi buat mainin spesial spin',
|
||||
textAlign: TextAlign.center,
|
||||
style: AppStyle.lg.copyWith(
|
||||
color: AppColor.textWhite,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPrizeListContent() {
|
||||
return Container(
|
||||
margin: const EdgeInsets.all(20),
|
||||
@@ -892,17 +837,15 @@ class _FerrisWheelPageState extends State<FerrisWheelPage>
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'Nilai: ${prize.metadata['value'] ?? prize.weight}',
|
||||
'ID: ${prize.id}',
|
||||
style: AppStyle.sm.copyWith(
|
||||
color: AppColor.textSecondary,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'Stok: ${prize.stock}/${prize.maxStock}',
|
||||
'Game ID: ${prize.gameId}',
|
||||
style: AppStyle.xs.copyWith(
|
||||
color: prize.stock > 0
|
||||
? AppColor.success
|
||||
: AppColor.error,
|
||||
color: AppColor.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -921,7 +864,7 @@ class _FerrisWheelPageState extends State<FerrisWheelPage>
|
||||
borderRadius: BorderRadius.circular(15),
|
||||
),
|
||||
child: Text(
|
||||
'Weight: ${prize.weight}',
|
||||
'Hadiah',
|
||||
style: AppStyle.xs.copyWith(
|
||||
color: _getPrizeColor(prize, index),
|
||||
fontWeight: FontWeight.bold,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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'
|
||||
|
||||
@@ -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
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user