From b64df2d28ce775212450a9775e66a37d049c550a Mon Sep 17 00:00:00 2001 From: PetalCat Date: Thu, 17 Aug 2023 20:31:57 -0500 Subject: [PATCH] feat: add gpa calculator page --- lib/app/app_router.dart | 11 +- .../dashboard/domain/dashboard_domain.dart | 12 +- .../dashboard_page/dashboard_page.dart | 7 +- .../wrapper_page/wrapper_page.dart | 3 +- .../gpa_calculator/domain/gpa_domain.dart | 21 ++++ .../gpa_calculator/domain/gpa_model.dart | 116 ++++++++++++++++++ .../gpa_calculator/presentation/gpa_page.dart | 100 +++++++++++++++ .../pirate_coins/domain/coins_domain.dart | 5 + 8 files changed, 262 insertions(+), 13 deletions(-) create mode 100644 lib/features/gpa_calculator/domain/gpa_domain.dart create mode 100644 lib/features/gpa_calculator/domain/gpa_model.dart create mode 100644 lib/features/gpa_calculator/presentation/gpa_page.dart diff --git a/lib/app/app_router.dart b/lib/app/app_router.dart index 63be2cd1..c6896f42 100644 --- a/lib/app/app_router.dart +++ b/lib/app/app_router.dart @@ -8,6 +8,7 @@ import "../features/auth/domain/auth_domain.dart"; import "../features/auth/presentation/auth_page/auth_page.dart"; import "../features/dashboard/presentation/dashboard_page/dashboard_page.dart"; import "../features/dashboard/presentation/wrapper_page/wrapper_page.dart"; +import "../features/gpa_calculator/presentation/gpa_page.dart"; import "../features/pirate_coins/presentation/pirate_coins_page/pirate_coins_page.dart"; import "../features/pirate_coins/presentation/stats_page/stats_page.dart"; @@ -47,7 +48,7 @@ class AppRouter extends _$AppRouter { ), AutoRoute( page: DashboardRoute.page, - path: "dashboard", + path: "", title: (context, route) => "Dashboard", ), AutoRoute( @@ -55,7 +56,11 @@ class AppRouter extends _$AppRouter { path: "stats", title: (context, route) => "Stats", ), - RedirectRoute(path: "*", redirectTo: "dashboard"), + AutoRoute( + page: GpaRoute.page, + path: "gpa-calculator", + title: (context, route) => "GPA Calculator", + ), ], title: (context, data) => "Pirate Code", ), @@ -65,6 +70,6 @@ class AppRouter extends _$AppRouter { title: (context, data) => "Login", initial: true, ), - RedirectRoute(path: "/*", redirectTo: "/dashboard"), + RedirectRoute(path: "/*", redirectTo: "/"), ]; } diff --git a/lib/features/dashboard/domain/dashboard_domain.dart b/lib/features/dashboard/domain/dashboard_domain.dart index c6434b54..b762cc0b 100644 --- a/lib/features/dashboard/domain/dashboard_domain.dart +++ b/lib/features/dashboard/domain/dashboard_domain.dart @@ -25,12 +25,12 @@ List applets(AppletsRef ref) { color: const Color.fromARGB(255, 122, 194, 129), location: const PirateCoinsRoute(), ), - // Applet( - // image: appletsFolder.gpaCalculator, - // color: const Color.fromARGB(255, 242, 184, 184), - // name: "GPA Calculator", - // location: const GpaCalculatorRoute(), - // ), + Applet( + image: appletsFolder.gpaCalculator, + color: const Color.fromARGB(255, 242, 184, 184), + name: "GPA Calculator", + location: const GpaRoute(), + ), // Applet( // image: appletsFolder.phsMap, // color: const Color.fromARGB(255, 178, 254, 186), diff --git a/lib/features/dashboard/presentation/dashboard_page/dashboard_page.dart b/lib/features/dashboard/presentation/dashboard_page/dashboard_page.dart index 2ee60775..854bf4d9 100644 --- a/lib/features/dashboard/presentation/dashboard_page/dashboard_page.dart +++ b/lib/features/dashboard/presentation/dashboard_page/dashboard_page.dart @@ -5,6 +5,7 @@ import "package:auto_route/auto_route.dart"; import "package:flutter/material.dart"; import "package:flutter_riverpod/flutter_riverpod.dart"; +import "../../../pirate_coins/domain/coins_domain.dart"; import "../../domain/dashboard_domain.dart"; import "../../domain/dashboard_model.dart"; @@ -116,7 +117,7 @@ class _NotificationBar extends StatelessWidget { } /// A button widget that navigates to a specified applet. -class _AppletButton extends StatelessWidget { +class _AppletButton extends ConsumerWidget { /// Create a new instance of [_AppletButton]. const _AppletButton({ required this.buttonData, @@ -129,7 +130,7 @@ class _AppletButton extends StatelessWidget { final Applet buttonData; @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { final imagePath = buttonData.image.path; final backgroundColor = buttonData.color; final title = buttonData.name; @@ -137,8 +138,8 @@ class _AppletButton extends StatelessWidget { return GestureDetector( onTap: () async { + ref.read(currentStageProvider.notifier).reset(); // Handle button tap here to navigate to the specified destination. - // TODO(ParkerH27): Find out why this doesn't change the URL. await context.router.push(destination); }, child: Card( diff --git a/lib/features/dashboard/presentation/wrapper_page/wrapper_page.dart b/lib/features/dashboard/presentation/wrapper_page/wrapper_page.dart index 9115f743..9f06dff7 100644 --- a/lib/features/dashboard/presentation/wrapper_page/wrapper_page.dart +++ b/lib/features/dashboard/presentation/wrapper_page/wrapper_page.dart @@ -33,6 +33,7 @@ class WrapperPage extends StatelessWidget { PirateCoinsRoute(), StatsRoute(), // TODO(lishaduck): Make this accessible. DashboardRoute(), + GpaRoute(), ], duration: const Duration(milliseconds: 200), transitionBuilder: (context, child, animation) { @@ -126,7 +127,7 @@ class _ExpandedWrapper extends ConsumerWidget { child: Text( "Pattonville Pirates", style: GoogleFonts.mrDafoe( - color: const Color.fromARGB(255, 11, 70, 24), + color: const Color.fromARGB(255, 9, 56, 19), shadows: [ Shadow( color: theme.colorScheme.shadow.withOpacity(0.5), diff --git a/lib/features/gpa_calculator/domain/gpa_domain.dart b/lib/features/gpa_calculator/domain/gpa_domain.dart new file mode 100644 index 00000000..90a1559a --- /dev/null +++ b/lib/features/gpa_calculator/domain/gpa_domain.dart @@ -0,0 +1,21 @@ +import "package:riverpod_annotation/riverpod_annotation.dart"; + +import "gpa_model.dart"; + +part "gpa_domain.g.dart"; + +/// Get the state for the GPA calculator. +@riverpod +class Gpa extends _$Gpa { + @override + Course build(int hour) { + return Course(hour: hour, grade: _defaultGrade); + } + + final _defaultGrade = const LetterGrade.a(); + + /// Update the course's grade. + void updateGrade(LetterGrade? grade) { + state = state.copyWith(grade: grade ?? _defaultGrade); + } +} diff --git a/lib/features/gpa_calculator/domain/gpa_model.dart b/lib/features/gpa_calculator/domain/gpa_model.dart new file mode 100644 index 00000000..aa7c484d --- /dev/null +++ b/lib/features/gpa_calculator/domain/gpa_model.dart @@ -0,0 +1,116 @@ +import "package:freezed_annotation/freezed_annotation.dart"; + +part "gpa_model.freezed.dart"; + +/// A course. +@freezed +@immutable +sealed class Course with _$Course { + /// Create a new, immutable instance of [Course]. + const factory Course({ + /// The hour the course takes place. + required int hour, + + // /// The name of the course. + // required String name, + + /// The grade of the course. + required LetterGrade grade, + }) = _Course; +} + +/// A grade. +sealed class LetterGrade { + /// An 'A'. + const factory LetterGrade.a({bool isHonors}) = _A; + + /// A 'B'. + const factory LetterGrade.b({bool isHonors}) = _B; + + /// A 'C'. + const factory LetterGrade.c({bool isHonors}) = _C; + + /// A 'D'. + const factory LetterGrade.d({bool isHonors}) = _D; + + /// An 'F'. + const factory LetterGrade.f({bool isHonors}) = _F; + + /// The GPA value for the letter grade. + int get value; +} + +/// An 'A'. +@freezed +@immutable +class _A with _$_A implements LetterGrade { + /// Create a new, immutable [LetterGrade.a]. + const factory _A({ + @Default(false) bool isHonors, + }) = __A; + + const _A._(); + + @override + int get value => isHonors ? 5 : 4; +} + +/// A 'B'. +@freezed +@immutable +class _B with _$_B implements LetterGrade { + /// Create a new, immutable [LetterGrade.b]. + const factory _B({ + @Default(false) bool isHonors, + }) = __B; + + const _B._(); + + @override + int get value => isHonors ? 4 : 3; +} + +/// A 'C'. +@freezed +@immutable +class _C with _$_C implements LetterGrade { + /// Create a new, immutable [LetterGrade.c]. + const factory _C({ + @Default(false) bool isHonors, + }) = __C; + + const _C._(); + + @override + int get value => isHonors ? 3 : 2; +} + +/// A 'D'. +@freezed +@immutable +class _D with _$_D implements LetterGrade { + /// Create a new, immutable [LetterGrade.d]. + const factory _D({ + @Default(false) bool isHonors, + }) = __D; + + const _D._(); + + @override + int get value => 1; +} + +/// An 'F'. +@freezed +@immutable +class _F with _$_F implements LetterGrade { + /// Create a new, immutable [LetterGrade.f]. + const factory _F({ + @Default(false) bool isHonors, + }) = __F; + + const _F._(); + + @override + int get value => 0; +} diff --git a/lib/features/gpa_calculator/presentation/gpa_page.dart b/lib/features/gpa_calculator/presentation/gpa_page.dart new file mode 100644 index 00000000..4aa2858d --- /dev/null +++ b/lib/features/gpa_calculator/presentation/gpa_page.dart @@ -0,0 +1,100 @@ +/// The auth feature. +library; + +import "package:auto_route/auto_route.dart"; +import "package:flutter/material.dart"; +import "package:flutter_riverpod/flutter_riverpod.dart"; + +import "../domain/gpa_domain.dart"; +import "../domain/gpa_model.dart"; + +/// The page located at `/login/` +@RoutePage() +class GpaPage extends ConsumerStatefulWidget { + /// Create a new instance of [GpaPage]. + const GpaPage({super.key}); + + @override + ConsumerState createState() => _GpaPageState(); +} + +class _GpaPageState extends ConsumerState { + final _formKey = GlobalKey(); + final _hours = 7; + + @override + Widget build(BuildContext context) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SizedBox( + width: 300, + child: Form( + key: _formKey, + child: Column( + children: [ + for (var hour = 0; hour < _hours; hour++) + Dropdown( + hour: hour, + ), + ], + ), + ), + ), + ElevatedButton( + onPressed: () { + if (_formKey.currentState?.validate.call() ?? false) { + var total = 0; + for (var hour = 0; hour < _hours; hour++) { + final grade = ref.read(gpaProvider(hour)).grade; + total += grade.value; + } + + final gpa = total / _hours; + + // TODO(lishaduck): Add an extension on context. + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text("Calculated GPA: $gpa")), + ); + } + }, + child: const Text("Calculate GPA"), + ), + ], + ), + ); + } +} + +/// Dropdown widget for picking your letter grade. +class Dropdown extends ConsumerStatefulWidget { + /// Create a new instance of [Dropdown]. + const Dropdown({required this.hour, super.key}); + + /// The hour the course is taken. + final int hour; + + @override + ConsumerState createState() => _DropdownState(); +} + +class _DropdownState extends ConsumerState { + @override + Widget build(BuildContext context) { + final value = ref.watch(gpaProvider(widget.hour)); + final valueNotifier = ref.watch(gpaProvider(widget.hour).notifier); + + return DropdownButtonFormField( + value: value.grade, + items: const [ + DropdownMenuItem(value: LetterGrade.a(), child: Text("A")), + DropdownMenuItem(value: LetterGrade.b(), child: Text("B")), + DropdownMenuItem(value: LetterGrade.c(), child: Text("C")), + DropdownMenuItem(value: LetterGrade.d(), child: Text("D")), + DropdownMenuItem(value: LetterGrade.f(), child: Text("F")), + ], + onChanged: valueNotifier.updateGrade, + ); + } +} diff --git a/lib/features/pirate_coins/domain/coins_domain.dart b/lib/features/pirate_coins/domain/coins_domain.dart index c1e4f4f5..b879e99c 100644 --- a/lib/features/pirate_coins/domain/coins_domain.dart +++ b/lib/features/pirate_coins/domain/coins_domain.dart @@ -75,4 +75,9 @@ class CurrentStage extends _$CurrentStage { void goToViewCoinsStage(int student) { state = ViewCoinsStage(student: student); } + + /// Reset the stage back to the default. + void reset() { + state = const PickStudentStage(); + } }