From ef728981c9f21b0bbcbbacacac40f47d109b9d64 Mon Sep 17 00:00:00 2001 From: Koen Van Looveren Date: Tue, 23 Jan 2024 17:48:21 +0100 Subject: [PATCH] feat(raffle): Option to enter user from the admin screen directly --- lib/navigation/main_navigator.dart | 6 + lib/repo/raffle_repo.dart | 33 +++- lib/screen/raffle_winner_picker_screen.dart | 159 ++++++++---------- .../raffle_winner_picker_viewmodel.dart | 18 +- lib/widget/general/button.dart | 77 +++++---- lib/widget/raffle/add_participant_dialog.dart | 76 +++++++++ lib/widget/raffle/custom_fortune_wheel.dart | 58 +++++++ .../social_login/social_login_button.dart | 2 +- pubspec.lock | 16 ++ pubspec.yaml | 1 + 10 files changed, 322 insertions(+), 124 deletions(-) create mode 100644 lib/widget/raffle/add_participant_dialog.dart create mode 100644 lib/widget/raffle/custom_fortune_wheel.dart diff --git a/lib/navigation/main_navigator.dart b/lib/navigation/main_navigator.dart index a56b25e..0f2a1df 100644 --- a/lib/navigation/main_navigator.dart +++ b/lib/navigation/main_navigator.dart @@ -1,6 +1,7 @@ import 'package:another_flushbar/flushbar.dart'; import 'package:flutter/material.dart'; import 'package:flutter_belgium/navigation/page_route/no_transition_page_route.dart'; +import 'package:flutter_belgium/widget/raffle/add_participant_dialog.dart'; import 'package:flutter_navigation_generator_annotations/flutter_navigation_generator_annotations.dart'; import 'package:injectable/injectable.dart'; import 'package:flutter_belgium/navigation/main_navigator.navigator.dart'; @@ -54,4 +55,9 @@ class MainNavigator with BaseNavigator { leftBarIndicatorColor: Colors.blue, ).show(context); } + + Future goToAddParticipantDialog() => showDialog( + context: context, + builder: (context) => const AddParticipantDialog(), + ); } diff --git a/lib/repo/raffle_repo.dart b/lib/repo/raffle_repo.dart index 27c4bfa..1e13e8d 100644 --- a/lib/repo/raffle_repo.dart +++ b/lib/repo/raffle_repo.dart @@ -6,6 +6,7 @@ import 'package:flutter_belgium/model/data/raffle/winner.dart'; import 'package:flutter_belgium/repo/login_repo.dart'; import 'package:injectable/injectable.dart'; import 'package:rxdart/rxdart.dart'; +import 'package:uuid/uuid.dart'; @lazySingleton abstract class RaffleRepository { @@ -32,6 +33,11 @@ abstract class RaffleRepository { required String raffleId, required RaffleParticipant winner, }); + + Future manuallyEnterRaffle({ + required String raffleId, + required String name, + }); } class _RaffleRepository implements RaffleRepository { @@ -87,7 +93,10 @@ class _RaffleRepository implements RaffleRepository { userUid: _loginRepository.userId!, name: _loginRepository.userName!, ); - await _firebaseFirestore.collection('raffle').doc(raffleId).collection('participants').doc(_loginRepository.userId).set(participant.toJson()); + await _addParticipantToRaffle( + raffleId: raffleId, + raffleParticipant: participant, + ); } @override @@ -101,4 +110,26 @@ class _RaffleRepository implements RaffleRepository { @override Stream hasWonRaffle(String raffleId) => _firebaseFirestore.collection('raffle').doc(raffleId).collection('winners').doc(_loginRepository.userId).snapshots().map((event) => event.exists); + + @override + Future manuallyEnterRaffle({ + required String raffleId, + required String name, + }) async { + final participant = RaffleParticipant( + userUid: 'manual_${const Uuid().v4()}', + name: name, + ); + await _addParticipantToRaffle( + raffleId: raffleId, + raffleParticipant: participant, + ); + } + + Future _addParticipantToRaffle({ + required String raffleId, + required RaffleParticipant raffleParticipant, + }) async { + await _firebaseFirestore.collection('raffle').doc(raffleId).collection('participants').doc(raffleParticipant.userUid).set(raffleParticipant.toJson()); + } } diff --git a/lib/screen/raffle_winner_picker_screen.dart b/lib/screen/raffle_winner_picker_screen.dart index 2abc8d5..d0c332d 100644 --- a/lib/screen/raffle_winner_picker_screen.dart +++ b/lib/screen/raffle_winner_picker_screen.dart @@ -1,14 +1,11 @@ -import 'dart:math'; - import 'package:flutter/material.dart'; import 'package:flutter_belgium/style/theme.dart'; -import 'package:flutter_belgium/style/theme_duration.dart'; import 'package:flutter_belgium/viewmodel/raffle_winner_picker_viewmodel.dart'; import 'package:flutter_belgium/widget/general/button.dart'; import 'package:flutter_belgium/widget/general/loading.dart'; import 'package:flutter_belgium/widget/raffle/custom_confetti.dart'; +import 'package:flutter_belgium/widget/raffle/custom_fortune_wheel.dart'; import 'package:flutter_navigation_generator_annotations/flutter_navigation_generator_annotations.dart'; -import 'package:flutter_fortune_wheel/flutter_fortune_wheel.dart'; import 'package:flutter_belgium/di/injectable.dart'; import 'package:flutter_belgium/widget/provider/provider_widget.dart'; @@ -48,49 +45,22 @@ class RaffleWinnerPickerScreen extends StatelessWidget { if (viewModel.isLoading) { return Container(); } - if (!viewModel.hasEnoughParticipants) { - return Center( - child: Text('Not enough participants, ${viewModel.participants.length} participant(s) entered this raffle. (min 2 required)'), - ); - } return Column( children: [ Expanded( - child: FortuneWheel( - selected: viewModel.selectedIndexStream, - animateFirst: false, - duration: ThemeDuration.raffleWheelDuration, - rotationCount: Random().nextInt(10) + 20, - indicators: const [ - FortuneIndicator( - alignment: Alignment.topCenter, - child: TriangleIndicator( - color: ThemeColors.primary, - ), - ), - ], - physics: CircularPanPhysics( - duration: const Duration(seconds: 1), - curve: Curves.decelerate, - ), - items: [ - for (final participant in viewModel.participants) - FortuneItem( + child: Builder( + builder: (context) { + if (!viewModel.hasEnoughParticipants) { + return Center( child: Text( - participant.name, - style: TextStyle( - color: ThemeColors.primary, - fontSize: viewModel.participants.length > 30 ? 16 : 24, - fontWeight: FontWeight.bold, - ), - ), - style: const FortuneItemStyle( - color: ThemeColors.primaryUltraLight, // <-- custom circle slice fill color - borderColor: ThemeColors.primary, - borderWidth: 2, - ), - ), - ], + 'Not enough participants, ${viewModel.participants.length} participant(s) entered this raffle. (min ${viewModel.minRequiredParticipants} required)'), + ); + } + return CustomFortuneWheel( + selected: viewModel.selectedIndexStream, + participants: viewModel.participants, + ); + }, ), ), Text( @@ -115,57 +85,66 @@ class RaffleWinnerPickerScreen extends StatelessWidget { ], ), ), - if (viewModel.hasEnoughParticipants) ...[ - Container( - width: 350, - padding: const EdgeInsets.all(16), - color: ThemeColors.primaryUltraLight, - alignment: Alignment.center, - child: Builder(builder: (context) { - if (viewModel.isLoading) { - return const Loading(); - } - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Participants list (${viewModel.participants.length})', - style: const TextStyle( - fontSize: 24, + Container( + width: 350, + padding: const EdgeInsets.all(16), + color: ThemeColors.primaryUltraLight, + alignment: Alignment.center, + child: Builder(builder: (context) { + if (viewModel.isLoading) { + return const Loading(); + } + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: Text( + 'Participants list (${viewModel.participants.length})', + style: const TextStyle( + fontSize: 24, + ), + ), ), - ), - Expanded( - child: ListView.builder( - itemCount: viewModel.participants.length, - itemBuilder: (context, index) { - final item = viewModel.participants[index]; - return Padding( - padding: const EdgeInsets.symmetric(vertical: 12), - child: Text( - item.name, - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - ), - ), - ); - }, + Button( + onTap: viewModel.onAddParticipantTapped, + text: '+', + fullWidth: false, ), + ], + ), + Expanded( + child: ListView.builder( + itemCount: viewModel.participants.length, + itemBuilder: (context, index) { + final item = viewModel.participants[index]; + return Padding( + padding: const EdgeInsets.symmetric(vertical: 12), + child: Text( + item.name, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + ); + }, ), - if (viewModel.winnerName == null) ...[ - Center( - child: Button( - onTap: viewModel.onPickWinnerTapped, - text: 'Pick Winner', - fullWidth: false, - ), + ), + if (viewModel.hasEnoughParticipants && viewModel.winnerName == null) ...[ + Center( + child: Button( + onTap: viewModel.onPickWinnerTapped, + text: 'Pick Winner', + fullWidth: false, ), - ] - ], - ); - }), - ), - ], + ), + ] + ], + ); + }), + ), ], ); }), diff --git a/lib/viewmodel/raffle_winner_picker_viewmodel.dart b/lib/viewmodel/raffle_winner_picker_viewmodel.dart index 33ba363..d64a646 100644 --- a/lib/viewmodel/raffle_winner_picker_viewmodel.dart +++ b/lib/viewmodel/raffle_winner_picker_viewmodel.dart @@ -30,6 +30,8 @@ class RaffleWinnerPickerViewModel with ChangeNotifier { Stream get selectedIndexStream => _selectedIndexStreamController.stream; + int get minRequiredParticipants => 2; + List get participants => _lockedParticipants ?? _allowedParticipants; bool get isLoading => _raffle == null; @@ -38,7 +40,7 @@ class RaffleWinnerPickerViewModel with ChangeNotifier { bool get hasInactiveRaffle => _raffle?.active == false; - bool get hasEnoughParticipants => participants.length > 2; + bool get hasEnoughParticipants => participants.length >= minRequiredParticipants; RaffleWinnerPickerViewModel( this._raffleRepository, @@ -102,4 +104,18 @@ class RaffleWinnerPickerViewModel with ChangeNotifier { } _raffleRepository.setRaffleActive(raffleId: docId, active: true); } + + Future onAddParticipantTapped() async { + final docId = _raffle?.id; + if (docId == null) { + _mainNavigator.showError('Failed to make raffle active (no raffle available)'); + return; + } + final name = await _mainNavigator.goToAddParticipantDialog(); + if (name == null) return; + await _raffleRepository.manuallyEnterRaffle( + raffleId: docId, + name: name, + ); + } } diff --git a/lib/widget/general/button.dart b/lib/widget/general/button.dart index ce1e3d3..37607cd 100644 --- a/lib/widget/general/button.dart +++ b/lib/widget/general/button.dart @@ -18,7 +18,7 @@ class Button extends StatefulWidget { super.key, }) : child = null; - const Button.chidl({ + const Button.child({ required this.onTap, required this.child, this.color, @@ -34,37 +34,52 @@ class _ButtonState extends State