diff --git a/Dockerfile b/Dockerfile index aba3a596..c08e520f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM node:18.14.0 +FROM node:20-alpine EXPOSE 80 EXPOSE 8000 diff --git a/game/android/app/build.gradle b/game/android/app/build.gradle index 912a74e9..1ad2bd46 100644 --- a/game/android/app/build.gradle +++ b/game/android/app/build.gradle @@ -42,7 +42,7 @@ android { defaultConfig { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId 'com.cornellgo.CornellGO' - minSdkVersion 21 + minSdkVersion 22 targetSdkVersion 31 versionCode flutterVersionCode.toInteger() versionName flutterVersionName diff --git a/game/assets/icons/Podium1stRed.svg b/game/assets/icons/Podium1stRed.svg new file mode 100644 index 00000000..26248c19 --- /dev/null +++ b/game/assets/icons/Podium1stRed.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/game/assets/icons/Podium2ndRed.svg b/game/assets/icons/Podium2ndRed.svg new file mode 100644 index 00000000..2f9189a7 --- /dev/null +++ b/game/assets/icons/Podium2ndRed.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/game/assets/icons/Podium3rdRed.svg b/game/assets/icons/Podium3rdRed.svg new file mode 100644 index 00000000..552b1d3b --- /dev/null +++ b/game/assets/icons/Podium3rdRed.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/game/assets/icons/red1podium.svg b/game/assets/icons/red1podium.svg new file mode 100644 index 00000000..e50ce6c6 --- /dev/null +++ b/game/assets/icons/red1podium.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/game/assets/icons/red2podium.svg b/game/assets/icons/red2podium.svg new file mode 100644 index 00000000..ab2c666f --- /dev/null +++ b/game/assets/icons/red2podium.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/game/assets/icons/red3podium.svg b/game/assets/icons/red3podium.svg new file mode 100644 index 00000000..f00dbd92 --- /dev/null +++ b/game/assets/icons/red3podium.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/game/assets/icons/yellow1podium.svg b/game/assets/icons/yellow1podium.svg new file mode 100644 index 00000000..b837ed52 --- /dev/null +++ b/game/assets/icons/yellow1podium.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/game/assets/icons/yellow2podium.svg b/game/assets/icons/yellow2podium.svg new file mode 100644 index 00000000..c1a5ab7a --- /dev/null +++ b/game/assets/icons/yellow2podium.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/game/assets/icons/yellow3podium.svg b/game/assets/icons/yellow3podium.svg new file mode 100644 index 00000000..fb54de24 --- /dev/null +++ b/game/assets/icons/yellow3podium.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/game/assets/images/podium.svg b/game/assets/images/podium.svg new file mode 100644 index 00000000..6655d9d4 --- /dev/null +++ b/game/assets/images/podium.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/game/assets/images/podium_first.svg b/game/assets/images/podium_first.svg new file mode 100644 index 00000000..2293463e --- /dev/null +++ b/game/assets/images/podium_first.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/game/assets/images/podium_second.svg b/game/assets/images/podium_second.svg new file mode 100644 index 00000000..91829a5f --- /dev/null +++ b/game/assets/images/podium_second.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/game/assets/images/podium_third.svg b/game/assets/images/podium_third.svg new file mode 100644 index 00000000..cb7f9ebe --- /dev/null +++ b/game/assets/images/podium_third.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/game/lib/details_page/details_page.dart b/game/lib/details_page/details_page.dart index d3b55976..0a422e8d 100644 --- a/game/lib/details_page/details_page.dart +++ b/game/lib/details_page/details_page.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:game/home_page/home_page_widget.dart'; import 'package:game/main.dart'; +import 'package:game/navbar.dart'; import 'package:google_sign_in/google_sign_in.dart'; import 'package:game/utils/utility_functions.dart'; @@ -152,7 +153,7 @@ class _DetailsPageWidgetState extends State { Navigator.push( context, MaterialPageRoute( - builder: (context) => HomePageWidget())); + builder: (context) => BottomNavBar())); } } }, diff --git a/game/lib/global_leaderboard/global_leaderboard_widget.dart b/game/lib/global_leaderboard/global_leaderboard_widget.dart index faf6a524..12e6b973 100644 --- a/game/lib/global_leaderboard/global_leaderboard_widget.dart +++ b/game/lib/global_leaderboard/global_leaderboard_widget.dart @@ -3,62 +3,287 @@ import 'package:game/api/game_client_dto.dart'; import 'package:game/model/event_model.dart'; import 'package:game/model/group_model.dart'; import 'package:game/model/user_model.dart'; -import 'package:game/widget/back_btn.dart'; +import 'package:game/global_leaderboard/podium_widgets.dart'; import 'package:game/widget/leaderboard_cell.dart'; -import 'package:game/widget/leaderboard_user_cell.dart'; +import 'package:game/widget/podium_cell.dart'; import 'package:provider/provider.dart'; +/** + * This widget defines the leaderboard page. + */ class GlobalLeaderboardWidget extends StatefulWidget { - GlobalLeaderboardWidget({Key? key}) : super(key: key); + const GlobalLeaderboardWidget({Key? key}) : super(key: key); @override _GlobalLeaderboardWidgetState createState() => _GlobalLeaderboardWidgetState(); } +/** + * This widget is the mutable state of the global leaderboard. Currently, there + * are sample users, but it gets the top 10 users and displays them on the leaderboard! + */ class _GlobalLeaderboardWidgetState extends State { final scaffoldKey = GlobalKey(); + //SampleUsers is mock leaderboard data since the users fetch is not working properly. + List sampleUsers = [ + UpdateLeaderDataUserDto.fromJson({ + "userId": "user1", + "username": "user1_username", + "score": 100, + }), + UpdateLeaderDataUserDto.fromJson({ + "userId": "user2", + "username": "user2_username", + "score": 85, + }), + UpdateLeaderDataUserDto.fromJson({ + "userId": "user3", + "username": "user3_username", + "score": 120, + }), + UpdateLeaderDataUserDto.fromJson({ + "userId": "user4", + "username": "user4_username", + "score": 75, + }), + UpdateLeaderDataUserDto.fromJson({ + "userId": "user5", + "username": "user5_username", + "score": 95, + }), + UpdateLeaderDataUserDto.fromJson({ + "userId": "user6", + "username": "user6_username", + "score": 110, + }), + UpdateLeaderDataUserDto.fromJson({ + "userId": "user7", + "username": "user7_username", + "score": 90, + }), + UpdateLeaderDataUserDto.fromJson({ + "userId": "user8", + "username": "user8_username", + "score": 80, + }), + UpdateLeaderDataUserDto.fromJson({ + "userId": "user9", + "username": "user9_username", + "score": 70, + }), + UpdateLeaderDataUserDto.fromJson({ + "userId": "user10", + "username": "user10_username", + "score": 115, + }), + UpdateLeaderDataUserDto.fromJson({ + "userId": "user11", + "username": "user11_username", + "score": 60, + }), + UpdateLeaderDataUserDto.fromJson({ + "userId": "user12", + "username": "user12_username", + "score": 130, + }), + UpdateLeaderDataUserDto.fromJson({ + "userId": "user13", + "username": "user13_username", + "score": 55, + }), + UpdateLeaderDataUserDto.fromJson({ + "userId": "user14", + "username": "user14_username", + "score": 105, + }), + UpdateLeaderDataUserDto.fromJson({ + "userId": "user15", + "username": "user15_username", + "score": 75, + }), + ]; + + //Thhis user represents the users status. This is mock data for now. + UserDto sampleUserData = UserDto.fromJson({ + "user": { + "id": "user6", + "username": "example_username", + "major": "Computer Science", + "year": "Senior", + "score": 100, + "groupId": "group123", + "rewardIds": ["reward1", "reward2"], + "trackedEventIds": ["event1", "event2"], + "authType": "google" + } + }); + @override Widget build(BuildContext context) { + var leaderboardStyle = TextStyle( + color: Colors.white, + fontFamily: 'Lato', + fontSize: 24.0, + fontWeight: FontWeight.w700, + height: 29.0 / 24.0, + letterSpacing: 0.0, + ); return Scaffold( - key: scaffoldKey, - floatingActionButton: backBtn(scaffoldKey, context, "Global Leaderboard"), - backgroundColor: Colors.black, - body: Padding( - padding: const EdgeInsets.only(top: 150), - child: Container( - child: Padding( - padding: const EdgeInsets.only(left: 8.0, right: 8.0), - child: Consumer3( - builder: + key: scaffoldKey, + backgroundColor: Color(0xFFE95755), + body: Padding( + padding: const EdgeInsets.only(top: 0), + child: Column( + children: [ + //Title Container + Container( + height: 29.0, + margin: EdgeInsets.only(top: 51.0, left: 25), + child: Text( + "Leaderboard", + style: leaderboardStyle, + ), + ), + //Podium Container + Consumer3(builder: (context, myGroupModel, myEventModel, myUserModel, child) { - int position = 1; - if (myGroupModel.curEventId == null) return ListView(); + //Loading in the lists and then creating podiumList of top 3 final List list = myEventModel.getTopPlayersForEvent('', 1000); - return Column(children: [ - for (int i = 0; i < list.length; i++) - if (myUserModel.userData?.id != null && - myUserModel.userData!.id == list.elementAt(i).userId) - leaderBoardUserCell(context, list.elementAt(i).username, - i + 1, list.length, list.elementAt(i).score), - Expanded( - child: ListView( - shrinkWrap: true, - scrollDirection: Axis.vertical, - children: [ - for (UpdateLeaderDataUserDto user in list) - leaderBoardCell(context, user.username, position++, - user.score, user.userId == myUserModel.userData?.id) - ], - )) - ]); - }, - ), + ; + list.sort((a, b) => b.score.compareTo(a.score)); + List podiumList = + list.sublist(0, list.length >= 3 ? 3 : list.length); + return Container( + width: 328, + height: 213, + margin: EdgeInsets.only(top: 24, left: 25), + child: Row(children: [ + Column( + children: [ + SizedBox(height: 26), + podiumList.length > 1 + ? podiumCell(context, podiumList[1].username, + podiumList[1].score) + : podiumCell(context, "", 0), + SizedBox(height: 12), + (podiumList.length > 1 && + podiumList[1].userId == sampleUserData.id) + ? SecondPodiumYellow() + : SecondPodiumRed(), + ], + ), + SizedBox(width: 5), + Column( + children: [ + podiumList.length > 0 + ? podiumCell(context, podiumList[0].username, + podiumList[0].score) + : podiumCell(context, "", 0), + SizedBox(height: 12), + (podiumList.length > 0 && + podiumList[0].userId == sampleUserData.id) + ? FirstPodiumYellow() + : FirstPodiumRed(), + ], + ), + SizedBox(width: 5), + Column( + children: [ + SizedBox(height: 50), + podiumList.length > 2 + ? podiumCell(context, podiumList[2].username, + podiumList[2].score) + : podiumCell(context, "", 0), + SizedBox(height: 12), + (podiumList.length > 2 && + podiumList[2].userId == sampleUserData.id) + ? ThirdPodiumYellow() + : ThirdPodiumRed(), + ], + ), + ]), + ); + }), + //Leaderboard Container + Expanded( + child: Padding( + padding: + const EdgeInsets.only(left: 33.0, right: 8.0, top: 1.0), + child: Consumer3( + builder: (context, myGroupModel, myEventModel, myUserModel, + child) { + int position = 4; + // Use this line below to retrieve actual data + final List list = + myEventModel.getTopPlayersForEvent('', 1000); + // final List list = sampleUsers; + list.sort((a, b) => b.score.compareTo(a.score)); + list.removeRange(0, list.length >= 3 ? 3 : list.length); + return Container( + width: 345.0, + height: 446.0, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.only( + topLeft: Radius.circular(10.0), + topRight: Radius.circular(10.0), + ), + ), + child: Container( + width: 283.05, + height: 432.0, + child: Expanded( + child: ListView( + shrinkWrap: true, + scrollDirection: Axis.vertical, + children: [ + for (UpdateLeaderDataUserDto user in list) + Padding( + padding: const EdgeInsets.only( + left: 30.95, + right: 30.95, + bottom: 16.0), + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(10.0), + topRight: Radius.circular(10.0), + bottomLeft: Radius.circular(10.0), + bottomRight: Radius.circular(10.0), + ), + color: Colors.white, + boxShadow: [ + BoxShadow( + color: Color(0x40000000), + offset: + Offset(0.0, 1.7472529411315918), + blurRadius: 6.989011764526367, + ), + ], + ), + child: leaderBoardCell( + context, + user.username, + position++, + user.score, + user.userId == sampleUserData.id, + ), + ), + ), + ], + ), + ), + ), + ); + }, + ), + ), + ) + ], ), - ), - ), - ); + )); } } diff --git a/game/lib/global_leaderboard/podium_widgets.dart b/game/lib/global_leaderboard/podium_widgets.dart new file mode 100644 index 00000000..7112aa5b --- /dev/null +++ b/game/lib/global_leaderboard/podium_widgets.dart @@ -0,0 +1,97 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; + +/** + * This file contains the 6 possible podium widgets to appear in the leaderboard + * page. For each podium position (1,2,3) there is a red and yellow widget, + * which is yellow if that is the user's current spot and red otherwise. + */ +class FirstPodiumRed extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Center( + child: Container( + width: 106, + height: 112, + child: SvgPicture.asset( + 'assets/icons/red1podium.svg', + semanticsLabel: '1st Red Podium', + ), + )); + } +} + +class FirstPodiumYellow extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Center( + child: Container( + width: 106, + height: 112, + child: SvgPicture.asset( + 'assets/icons/yellow1podium.svg', + semanticsLabel: '1st Yellow Podium', + ), + )); + } +} + +class SecondPodiumRed extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Center( + child: Container( + width: 106, + height: 86, + child: SvgPicture.asset( + 'assets/icons/red2podium.svg', + semanticsLabel: '2nd Red Podium', + ), + )); + } +} + +class SecondPodiumYellow extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Center( + child: Container( + width: 106, + height: 86, + child: SvgPicture.asset( + 'assets/icons/yellow2podium.svg', + semanticsLabel: '2nd Yellow Podium', + ), + )); + } +} + +class ThirdPodiumYellow extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Center( + child: Container( + width: 106, + height: 62, + child: SvgPicture.asset( + 'assets/icons/yellow3podium.svg', + semanticsLabel: '3rd Yellow Podium', + ), + )); + } +} + +class ThirdPodiumRed extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Center( + child: Container( + width: 106, + height: 62, + child: SvgPicture.asset( + 'assets/icons/red3podium.svg', + semanticsLabel: '3rd Red Podium', + ), + )); + } +} diff --git a/game/lib/main.dart b/game/lib/main.dart index fec8615f..df042ddf 100644 --- a/game/lib/main.dart +++ b/game/lib/main.dart @@ -4,6 +4,7 @@ import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:game/api/game_api.dart'; import 'package:game/challenges/challenges_widget.dart'; import 'package:game/gameplay/gameplay_page.dart'; +import 'package:game/global_leaderboard/global_leaderboard_widget.dart'; import 'package:game/journeys/journeys_page.dart'; import 'package:game/login/login_page.dart'; import 'package:game/model/challenge_model.dart'; @@ -80,7 +81,7 @@ class MyApp extends StatelessWidget { supportedLocales: const [Locale('en', '')], theme: ThemeData( fontFamily: 'Poppins', primarySwatch: ColorPalette.BigRed), - home: BottomNavBar(), + home: SplashPageWidget(), ))); } } diff --git a/game/lib/navigation_page/bottom_navbar.dart b/game/lib/navigation_page/bottom_navbar.dart index 58229cf2..63b1f447 100644 --- a/game/lib/navigation_page/bottom_navbar.dart +++ b/game/lib/navigation_page/bottom_navbar.dart @@ -16,10 +16,7 @@ class _BottomNavBarState extends State { TextStyle(fontSize: 30, fontWeight: FontWeight.bold); static const List _widgetOptions = [ HomeNavBar(), - Text( - 'Leaderboard', - style: optionStyle, - ), + GlobalLeaderboardWidget(), Text( 'Profile', style: optionStyle, diff --git a/game/lib/widget/leaderboard_cell.dart b/game/lib/widget/leaderboard_cell.dart index d38b358f..5ccbbe56 100644 --- a/game/lib/widget/leaderboard_cell.dart +++ b/game/lib/widget/leaderboard_cell.dart @@ -1,67 +1,93 @@ import 'package:flutter/material.dart'; import 'package:game/utils/utility_functions.dart'; +/** + * Widget that represents each individual leaderboard entry + * @param name: name of the user + * @param position: the place that the user is in overall + * @param points: the number of points the user has + * @param isUser: whether the cell is the current user and should be hilighted + */ Widget leaderBoardCell( context, String name, int position, int points, bool isUser) { - Color Carnelian = Color(0xFFB31B1B); + //Creating the styles to use for the position, name, and points var posStyle = TextStyle( - fontWeight: FontWeight.bold, - fontSize: 40, - color: (isUser) ? Carnelian : Colors.white); + fontFamily: 'Inter', + fontSize: 23, + fontWeight: FontWeight.w500, + height: 1.42, + letterSpacing: 0.0, + color: Colors.black, + ); var nameStyle = TextStyle( - fontWeight: (isUser) ? FontWeight.w900 : FontWeight.w800, - fontSize: 22, - color: Colors.white); + fontFamily: 'Inter', + fontSize: 14, + fontWeight: FontWeight.w500, + height: 1.5, + letterSpacing: 0, + color: Colors.black, + ); var pointStyle = TextStyle( - fontWeight: FontWeight.normal, - fontSize: 18, - color: (isUser) ? Carnelian : Colors.white, - fontStyle: FontStyle.italic); - return Padding( - padding: const EdgeInsets.all(8.0), - child: Container( - decoration: - BoxDecoration(border: Border(bottom: BorderSide(color: Colors.grey))), - width: MediaQuery.of(context).size.width, - child: Row( - children: [ - Row( - children: [ - Container( - child: Row( + fontFamily: 'Inter', + fontSize: 11, + fontWeight: FontWeight.w400, + height: 1.5455, + letterSpacing: 0, + color: Colors.black, + ); + + return Container( + decoration: BoxDecoration( + color: (isUser) ? Color.fromARGB(255, 251, 227, 195) : Colors.white, + borderRadius: BorderRadius.circular(10.0), + ), + child: Padding( + padding: const EdgeInsets.all(8.74), + child: ClipRRect( + child: Container( + width: 266, + height: 34, + child: Row( + children: [ + Row( children: [ - Text(position.toString(), style: posStyle), + Container( + child: Row( + children: [ + Text(position.toString(), + style: posStyle, textAlign: TextAlign.center), + Padding( + padding: const EdgeInsets.only(left: 12.0), + child: Container( + width: 30, + height: 30, + decoration: BoxDecoration( + color: constructColorFromUserName(name), + borderRadius: BorderRadius.circular(15)), + ), + ) + ], + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + ), + ), Padding( - padding: const EdgeInsets.only(left: 12.0), + padding: const EdgeInsets.only(left: 16.0), child: Container( - width: 30, - height: 30, - decoration: BoxDecoration( - color: constructColorFromUserName(name), - borderRadius: BorderRadius.circular(15)), + child: Text(name, style: nameStyle), ), - ) + ), ], - mainAxisAlignment: MainAxisAlignment.spaceEvenly, ), - ), - Padding( - padding: const EdgeInsets.only(left: 16.0), - child: Container( - child: Text(name, style: nameStyle), - ), - ), - ], - ), - Container( - child: Text( - points.toString(), - style: pointStyle, + Container( + child: Text( + points.toString() + " points", + style: pointStyle, + ), + ) + ], + mainAxisAlignment: MainAxisAlignment.spaceBetween, ), - ) - ], - mainAxisAlignment: MainAxisAlignment.spaceBetween, - ), - ), - ); + ), + ), + )); } diff --git a/game/lib/widget/podium_cell.dart b/game/lib/widget/podium_cell.dart new file mode 100644 index 00000000..f4a0eda3 --- /dev/null +++ b/game/lib/widget/podium_cell.dart @@ -0,0 +1,46 @@ +import 'package:flutter/material.dart'; +import 'package:game/utils/utility_functions.dart'; + +/** + * This widget represents the users which are in the top 3 positions! They have + * a different representation than the other users in the leaderboard because + * they are on the podium. + * @param name: the name of the user + * @param points: the number of points the user has scored + */ +Widget podiumCell(context, String name, int points) { + var nameStyle = TextStyle( + color: Color(0xFF000000), + fontFamily: 'Inter', + fontSize: 11.392, + fontStyle: FontStyle.normal, + fontWeight: FontWeight.w500, + height: 1.5, + ); + + var pointStyle = TextStyle( + color: Color(0xFF000000), + fontFamily: 'Inter', + fontSize: 11.392, + fontStyle: FontStyle.normal, + fontWeight: FontWeight.w400, + height: 1.5, + ); + + if (name.length > 7) name = name.substring(0, 7) + "..."; + return Container( + width: 78, + height: 88.824, + child: Column(children: [ + Container( + width: 49.128, + height: 49.128, + decoration: BoxDecoration( + color: constructColorFromUserName(name), + borderRadius: BorderRadius.circular(49.128)), + ), + Text(name, style: nameStyle, textAlign: TextAlign.center), + Text(points.toString() + " points", + style: pointStyle, textAlign: TextAlign.center), + ])); +} diff --git a/game/pubspec.yaml b/game/pubspec.yaml index 50882a2d..5c7b7f24 100644 --- a/game/pubspec.yaml +++ b/game/pubspec.yaml @@ -25,7 +25,6 @@ dependencies: sdk: flutter flutter_localizations: sdk: flutter - flutter_svg: ^2.0.5 sliding_up_panel: ^2.0.0+1 velocity_x: flutter_blurhash: @@ -50,6 +49,7 @@ dependencies: flutter_map: ^6.0.1 latlong2: ^0.9.0 device_info_plus: ^9.1.0 + flutter_svg: ^2.0.8 flutter_icons: android: true