diff --git a/.vscode/launch.json b/.vscode/launch.json index 12bff38c..116d1df2 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -4,6 +4,14 @@ // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ + { + "type": "node", + "request": "attach", + "name": "Docker: Attach to Node", + "localRoot": "${workspaceFolder}/server", + "remoteRoot": "/app/server", + "port": 9229 + }, { "name": "game", "cwd": "game", @@ -25,4 +33,4 @@ "flutterMode": "release" } ] -} \ No newline at end of file +} diff --git a/README.md b/README.md index 6e68a3a9..c75e50dd 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,16 @@ To access the Google Maps in the CornellGo app, you need two Google Maps API Key docker compose up --build ``` +### Run project with backend hot reloading + +``` +docker compose watch + +If you want to see logging, run the following in a different terminal: +docker compose logs --follow +Or look at your docker desktop app +``` + ### Start the container in the background ``` @@ -50,6 +60,12 @@ npx prisma studio npm run updateapi ``` +### Format all code files + +``` +npm run formatall +``` + ### Start the frontend (for debugging) ``` diff --git a/admin/public/favicon.ico b/admin/public/favicon.ico index 0e165520..5fd02590 100644 Binary files a/admin/public/favicon.ico and b/admin/public/favicon.ico differ diff --git a/admin/src/all.dto.ts b/admin/src/all.dto.ts index 58f9b7f3..c11350a7 100644 --- a/admin/src/all.dto.ts +++ b/admin/src/all.dto.ts @@ -102,10 +102,14 @@ export interface RequestAchievementDataDto { export interface LoginDto { idToken: string; - lat: number; - long: number; + noRegister: boolean; + latF?: number; + longF?: number; username?: string; year?: string; + college?: string; + major?: string; + interests?: string; aud?: LoginAudDto; enrollmentType: LoginEnrollmentTypeDto; } @@ -149,26 +153,16 @@ export interface SetCurrentChallengeDto { challengeId: string; } -export interface LeaderDto { - userId: string; - username: string; - score: number; -} - -export interface UpdateLeaderDataDto { - eventId: string; - offset: number; - users: LeaderDto[]; -} - export interface UpdateErrorDto { id: string; message: string; } -export interface RequestAllEventDataDto { - offset: number; - count: number; +export interface RequestFilteredEventsDto { + difficulty: string[]; + location: string[]; + category: string[]; + filterId: string[]; } export interface RequestEventDataDto { @@ -178,6 +172,25 @@ export interface RequestEventDataDto { export interface RequestEventLeaderDataDto { offset: number; count: number; + eventId?: string; +} + +export interface LeaderDto { + userId: string; + username: string; + score: number; +} + +export interface UpdateLeaderDataDto { + eventId?: string; + offset: number; + users: LeaderDto[]; +} + +export interface UpdateLeaderPositionDto { + playerId: string; + newTotalScore: number; + newEventScore: number; eventId: string; } @@ -290,18 +303,6 @@ export interface UpdateOrganizationDataDto { export interface CloseAccountDto {} -export interface SetUsernameDto { - newUsername: string; -} - -export interface SetMajorDto { - newMajor: string; -} - -export interface SetGraduationYearDto { - newYear: string; -} - export interface BanUserDto { userId: string; isBanned: boolean; @@ -338,6 +339,9 @@ export interface UserDto { enrollmentType?: UserEnrollmentTypeDto; email?: string; year?: string; + college?: string; + major?: string; + interests?: string[]; score?: number; isBanned?: boolean; groupId?: string; diff --git a/admin/src/components/Challenges.tsx b/admin/src/components/Challenges.tsx index 0b2d9728..a70da10d 100644 --- a/admin/src/components/Challenges.tsx +++ b/admin/src/components/Challenges.tsx @@ -131,7 +131,9 @@ function toForm(challenge: ChallengeDto) { { name: "Image URL", characterLimit: 2048, - value: challenge.imageUrl ?? "", + value: + challenge.imageUrl ?? + "https://upload.wikimedia.org/wikipedia/commons/b/b1/Missing-image-232x150.png", }, { name: "Awarding Distance (meters)", diff --git a/admin/src/components/ServerApi.tsx b/admin/src/components/ServerApi.tsx index 223807d2..173d4602 100644 --- a/admin/src/components/ServerApi.tsx +++ b/admin/src/components/ServerApi.tsx @@ -29,10 +29,6 @@ export class ServerApi { this.send("completedChallenge", data); } - requestGlobalLeaderData(data: dto.RequestGlobalLeaderDataDto) { - this.send("requestGlobalLeaderData", data); - } - updateChallengeData(data: dto.UpdateChallengeDataDto) { this.send("updateChallengeData", data); } @@ -41,6 +37,10 @@ export class ServerApi { this.send("requestEventData", data); } + requestFilteredEventIds(data: dto.RequestFilteredEventsDto) { + this.send("requestFilteredEventIds", data); + } + requestRecommendedEvents(data: dto.RequestRecommendedEventsDto) { this.send("requestRecommendedEvents", data); } @@ -189,4 +189,11 @@ export class ServerApi { this.socket.removeAllListeners("updateOrganizationData"); this.socket.on("updateOrganizationData", (data) => callback(data)); } + + onUpdateLeaderPosition( + callback: (data: dto.UpdateLeaderPositionDto) => void + ) { + this.socket.removeAllListeners("updateLeaderPosition"); + this.socket.on("updateLeaderPosition", (data) => callback(data)); + } } diff --git a/admin/src/components/ServerConnection.tsx b/admin/src/components/ServerConnection.tsx index 4d3f9c0b..762bd6e5 100644 --- a/admin/src/components/ServerConnection.tsx +++ b/admin/src/components/ServerConnection.tsx @@ -48,8 +48,10 @@ export function ServerConnectionProvider(props: { children: ReactNode }) { lat: 1, long: 1, username: "guest", + college: "Arts and Sciences", major: "Computer Science", year: "2025", + interests: "", aud: "web", enrollmentType: "UNDERGRADUATE", }); diff --git a/docker-compose.yml b/docker-compose.yml index 0d726cde..c0c143be 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -24,6 +24,7 @@ services: server: ports: - "8080:80" + - "9229:9229" environment: NO_SSL: true DEVELOPMENT: true @@ -35,6 +36,15 @@ services: postgres: condition: service_healthy build: . + develop: + watch: + - action: sync + path: ./server/ + target: /app/server/ + ignore: + - node_modules/ + - action: rebuild + path: server/package.json networks: - reference networks: diff --git a/game/android/app/google-services.json b/game/android/app/google-services.json index 60a30e64..30406c28 100644 --- a/game/android/app/google-services.json +++ b/game/android/app/google-services.json @@ -21,6 +21,22 @@ "certificate_hash": "d6e7da6b98f7884510d3826068e668c5bb7ec89b" } }, + { + "client_id": "757523123677-mdis2048vo39u7bjg7rjtm9r28a1tsto.apps.googleusercontent.com", + "client_type": 1, + "android_info": { + "package_name": "com.cornellgo.CornellGO", + "certificate_hash": "48942102c610163af43caab359452832e7bdc189" + } + }, + { + "client_id": "757523123677-s7lm7n8b1vmppnh92pige13uvp722jma.apps.googleusercontent.com", + "client_type": 1, + "android_info": { + "package_name": "com.cornellgo.CornellGO", + "certificate_hash": "e8ec9df2de92f3ff018926aba9448be15c464a4e" + } + }, { "client_id": "757523123677-2nv6haiqnvklhb134cgg5qe8bia4du4q.apps.googleusercontent.com", "client_type": 3 @@ -28,7 +44,7 @@ ], "api_key": [ { - "current_key": "AIzaSyDSKPt3T-sBGcaS5vQGw-FDkgOBTryxhUQ" + "current_key": "AIzaSyCNuTQ0eTjwXde0NMNMFaO6c3u5AYlNiXA" }, { "current_key": "AIzaSyBBGoQlbjlNCBfxiob-mkm1TCougaHqcu4" @@ -42,10 +58,10 @@ "client_type": 3 }, { - "client_id": "757523123677-cso74eimkjqkcuce69nrf4c637k3db6t.apps.googleusercontent.com", + "client_id": "757523123677-5uct36sksguq3v4ngeec8ofc31ipf8hl.apps.googleusercontent.com", "client_type": 2, "ios_info": { - "bundle_id": "com.cornellgo.game" + "bundle_id": "com.cornellgo.CornellGO" } } ] @@ -61,19 +77,11 @@ }, "oauth_client": [ { - "client_id": "757523123677-lhftm1kuh7k0uqkhc62d378um4nb5e3g.apps.googleusercontent.com", - "client_type": 1, - "android_info": { - "package_name": "com.cornellgo.game", - "certificate_hash": "d6e7da6b98f7884510d3826068e668c5bb7ec89b" - } - }, - { - "client_id": "757523123677-q8o2d3tahsmm9i68j1sf33ka49qsgalq.apps.googleusercontent.com", + "client_id": "757523123677-q8lt36h21g5kcmvvqe07f5krldkb4k4i.apps.googleusercontent.com", "client_type": 1, "android_info": { "package_name": "com.cornellgo.game", - "certificate_hash": "98efe77cdeb1f8f17e7c3634a8700ae7361aaa08" + "certificate_hash": "e8ec9df2de92f3ff018926aba9448be15c464a4e" } }, { @@ -83,7 +91,7 @@ ], "api_key": [ { - "current_key": "AIzaSyDSKPt3T-sBGcaS5vQGw-FDkgOBTryxhUQ" + "current_key": "AIzaSyCNuTQ0eTjwXde0NMNMFaO6c3u5AYlNiXA" }, { "current_key": "AIzaSyBBGoQlbjlNCBfxiob-mkm1TCougaHqcu4" @@ -97,10 +105,10 @@ "client_type": 3 }, { - "client_id": "757523123677-cso74eimkjqkcuce69nrf4c637k3db6t.apps.googleusercontent.com", + "client_id": "757523123677-5uct36sksguq3v4ngeec8ofc31ipf8hl.apps.googleusercontent.com", "client_type": 2, "ios_info": { - "bundle_id": "com.cornellgo.game" + "bundle_id": "com.cornellgo.CornellGO" } } ] @@ -109,4 +117,4 @@ } ], "configuration_version": "1" -} \ No newline at end of file +} diff --git a/game/android/app/src/main/AndroidManifest.xml b/game/android/app/src/main/AndroidManifest.xml index 852baa93..f9cdff15 100644 --- a/game/android/app/src/main/AndroidManifest.xml +++ b/game/android/app/src/main/AndroidManifest.xml @@ -3,7 +3,7 @@ - + diff --git a/game/android/app/src/main/res/mipmap-hdpi/launcher_icon.png b/game/android/app/src/main/res/mipmap-hdpi/launcher_icon.png new file mode 100644 index 00000000..7d7c2013 Binary files /dev/null and b/game/android/app/src/main/res/mipmap-hdpi/launcher_icon.png differ diff --git a/game/android/app/src/main/res/mipmap-mdpi/launcher_icon.png b/game/android/app/src/main/res/mipmap-mdpi/launcher_icon.png new file mode 100644 index 00000000..786c0c4c Binary files /dev/null and b/game/android/app/src/main/res/mipmap-mdpi/launcher_icon.png differ diff --git a/game/android/app/src/main/res/mipmap-xhdpi/launcher_icon.png b/game/android/app/src/main/res/mipmap-xhdpi/launcher_icon.png new file mode 100644 index 00000000..b332938f Binary files /dev/null and b/game/android/app/src/main/res/mipmap-xhdpi/launcher_icon.png differ diff --git a/game/android/app/src/main/res/mipmap-xxhdpi/launcher_icon.png b/game/android/app/src/main/res/mipmap-xxhdpi/launcher_icon.png new file mode 100644 index 00000000..03bba25a Binary files /dev/null and b/game/android/app/src/main/res/mipmap-xxhdpi/launcher_icon.png differ diff --git a/game/android/app/src/main/res/mipmap-xxxhdpi/launcher_icon.png b/game/android/app/src/main/res/mipmap-xxxhdpi/launcher_icon.png new file mode 100644 index 00000000..03fbcb88 Binary files /dev/null and b/game/android/app/src/main/res/mipmap-xxxhdpi/launcher_icon.png differ diff --git a/game/assets/icons/Podium1stRed.svg b/game/assets/icons/Podium1stRed.svg deleted file mode 100644 index 26248c19..00000000 --- a/game/assets/icons/Podium1stRed.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/game/assets/icons/Podium2ndRed.svg b/game/assets/icons/Podium2ndRed.svg deleted file mode 100644 index 2f9189a7..00000000 --- a/game/assets/icons/Podium2ndRed.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/game/assets/icons/Podium3rdRed.svg b/game/assets/icons/Podium3rdRed.svg deleted file mode 100644 index 552b1d3b..00000000 --- a/game/assets/icons/Podium3rdRed.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/game/assets/icons/achievementgold.svg b/game/assets/icons/achievementgold.svg new file mode 100644 index 00000000..ce365544 --- /dev/null +++ b/game/assets/icons/achievementgold.svg @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/game/assets/icons/achievementsilver.svg b/game/assets/icons/achievementsilver.svg new file mode 100644 index 00000000..09a3cc5a --- /dev/null +++ b/game/assets/icons/achievementsilver.svg @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/game/assets/icons/bronzestar.svg b/game/assets/icons/bronzestar.svg deleted file mode 100644 index 81569254..00000000 --- a/game/assets/icons/bronzestar.svg +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/game/assets/icons/goldstar.svg b/game/assets/icons/goldstar.svg deleted file mode 100644 index f51a4aa5..00000000 --- a/game/assets/icons/goldstar.svg +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/game/assets/icons/podium1blank.svg b/game/assets/icons/podium1blank.svg new file mode 100644 index 00000000..c97b9c42 --- /dev/null +++ b/game/assets/icons/podium1blank.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/game/assets/icons/podium1highlighted.svg b/game/assets/icons/podium1highlighted.svg deleted file mode 100644 index 5ddec862..00000000 --- a/game/assets/icons/podium1highlighted.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/game/assets/icons/podium1red.svg b/game/assets/icons/podium1red.svg deleted file mode 100644 index 72fa62c8..00000000 --- a/game/assets/icons/podium1red.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/game/assets/icons/podium1redblank.svg b/game/assets/icons/podium1redblank.svg deleted file mode 100644 index 27a48ac6..00000000 --- a/game/assets/icons/podium1redblank.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/game/assets/icons/podium1user.svg b/game/assets/icons/podium1user.svg new file mode 100644 index 00000000..5d951dbb --- /dev/null +++ b/game/assets/icons/podium1user.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/game/assets/icons/podium2blank.svg b/game/assets/icons/podium2blank.svg new file mode 100644 index 00000000..d4a80410 --- /dev/null +++ b/game/assets/icons/podium2blank.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/game/assets/icons/podium2highlighted.svg b/game/assets/icons/podium2highlighted.svg deleted file mode 100644 index 99bf3920..00000000 --- a/game/assets/icons/podium2highlighted.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/game/assets/icons/podium2red.svg b/game/assets/icons/podium2red.svg deleted file mode 100644 index 2baf7ca3..00000000 --- a/game/assets/icons/podium2red.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/game/assets/icons/podium2user.svg b/game/assets/icons/podium2user.svg new file mode 100644 index 00000000..612e3669 --- /dev/null +++ b/game/assets/icons/podium2user.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/game/assets/icons/podium3blank.svg b/game/assets/icons/podium3blank.svg new file mode 100644 index 00000000..9b7da4d6 --- /dev/null +++ b/game/assets/icons/podium3blank.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/game/assets/icons/podium3highlighted.svg b/game/assets/icons/podium3highlighted.svg deleted file mode 100644 index ac807851..00000000 --- a/game/assets/icons/podium3highlighted.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/game/assets/icons/podium3red.svg b/game/assets/icons/podium3red.svg deleted file mode 100644 index 62025d1e..00000000 --- a/game/assets/icons/podium3red.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/game/assets/icons/podium3user.svg b/game/assets/icons/podium3user.svg new file mode 100644 index 00000000..77158a2d --- /dev/null +++ b/game/assets/icons/podium3user.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/game/assets/icons/red1podium.svg b/game/assets/icons/red1podium.svg deleted file mode 100644 index e50ce6c6..00000000 --- a/game/assets/icons/red1podium.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/game/assets/icons/red2podium.svg b/game/assets/icons/red2podium.svg deleted file mode 100644 index ab2c666f..00000000 --- a/game/assets/icons/red2podium.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/game/assets/icons/red3podium.svg b/game/assets/icons/red3podium.svg deleted file mode 100644 index f00dbd92..00000000 --- a/game/assets/icons/red3podium.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/game/assets/icons/silverstar.svg b/game/assets/icons/silverstar.svg deleted file mode 100644 index 4144a8ce..00000000 --- a/game/assets/icons/silverstar.svg +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/game/assets/icons/yellow1podium.svg b/game/assets/icons/yellow1podium.svg deleted file mode 100644 index b837ed52..00000000 --- a/game/assets/icons/yellow1podium.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/game/assets/icons/yellow2podium.svg b/game/assets/icons/yellow2podium.svg deleted file mode 100644 index c1a5ab7a..00000000 --- a/game/assets/icons/yellow2podium.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/game/assets/icons/yellow3podium.svg b/game/assets/icons/yellow3podium.svg deleted file mode 100644 index fb54de24..00000000 --- a/game/assets/icons/yellow3podium.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/game/assets/images/challenge-completed-bg.svg b/game/assets/images/challenge-completed-bg.svg index ab2ce837..93b29e35 100644 --- a/game/assets/images/challenge-completed-bg.svg +++ b/game/assets/images/challenge-completed-bg.svg @@ -1,108 +1,62 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/game/assets/images/main_icon.png b/game/assets/images/main_icon.png index c1c53831..9fb25e7b 100644 Binary files a/game/assets/images/main_icon.png and b/game/assets/images/main_icon.png differ diff --git a/game/assets/images/yellow_bear_prof.svg b/game/assets/images/yellow_bear_prof.svg new file mode 100644 index 00000000..3266dda5 --- /dev/null +++ b/game/assets/images/yellow_bear_prof.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/game/ios/Podfile.lock b/game/ios/Podfile.lock index ca34db12..4c9cc639 100644 --- a/game/ios/Podfile.lock +++ b/game/ios/Podfile.lock @@ -1,25 +1,25 @@ PODS: - - AppAuth (1.7.3): - - AppAuth/Core (= 1.7.3) - - AppAuth/ExternalUserAgent (= 1.7.3) - - AppAuth/Core (1.7.3) - - AppAuth/ExternalUserAgent (1.7.3): + - AppAuth (1.7.4): + - AppAuth/Core (= 1.7.4) + - AppAuth/ExternalUserAgent (= 1.7.4) + - AppAuth/Core (1.7.4) + - AppAuth/ExternalUserAgent (1.7.4): - AppAuth/Core - device_info (0.0.1): - Flutter - device_info_plus (0.0.1): - Flutter - - Firebase (10.23.0): - - Firebase/Core (= 10.23.0) - - Firebase/Analytics (10.23.0): + - Firebase (10.24.0): + - Firebase/Core (= 10.24.0) + - Firebase/Analytics (10.24.0): - Firebase/Core - - Firebase/Core (10.23.0): + - Firebase/Core (10.24.0): - Firebase/CoreOnly - - FirebaseAnalytics (~> 10.23.0) - - Firebase/CoreOnly (10.23.0): - - FirebaseCore (= 10.23.0) - - FirebaseAnalytics (10.23.0): - - FirebaseAnalytics/AdIdSupport (= 10.23.0) + - FirebaseAnalytics (~> 10.24.0) + - Firebase/CoreOnly (10.24.0): + - FirebaseCore (= 10.24.0) + - FirebaseAnalytics (10.24.0): + - FirebaseAnalytics/AdIdSupport (= 10.24.0) - FirebaseCore (~> 10.0) - FirebaseInstallations (~> 10.0) - GoogleUtilities/AppDelegateSwizzler (~> 7.11) @@ -27,22 +27,22 @@ PODS: - GoogleUtilities/Network (~> 7.11) - "GoogleUtilities/NSData+zlib (~> 7.11)" - nanopb (< 2.30911.0, >= 2.30908.0) - - FirebaseAnalytics/AdIdSupport (10.23.0): + - FirebaseAnalytics/AdIdSupport (10.24.0): - FirebaseCore (~> 10.0) - FirebaseInstallations (~> 10.0) - - GoogleAppMeasurement (= 10.23.0) + - GoogleAppMeasurement (= 10.24.0) - GoogleUtilities/AppDelegateSwizzler (~> 7.11) - GoogleUtilities/MethodSwizzler (~> 7.11) - GoogleUtilities/Network (~> 7.11) - "GoogleUtilities/NSData+zlib (~> 7.11)" - nanopb (< 2.30911.0, >= 2.30908.0) - - FirebaseCore (10.23.0): + - FirebaseCore (10.24.0): - FirebaseCoreInternal (~> 10.0) - GoogleUtilities/Environment (~> 7.12) - GoogleUtilities/Logger (~> 7.12) - - FirebaseCoreInternal (10.23.0): + - FirebaseCoreInternal (10.24.0): - "GoogleUtilities/NSData+zlib (~> 7.8)" - - FirebaseInstallations (10.23.0): + - FirebaseInstallations (10.24.0): - FirebaseCore (~> 10.0) - GoogleUtilities/Environment (~> 7.8) - GoogleUtilities/UserDefaults (~> 7.8) @@ -55,9 +55,6 @@ PODS: - fluttertoast (0.0.2): - Flutter - Toast - - FMDB (2.7.9): - - FMDB/standard (= 2.7.9) - - FMDB/standard (2.7.9) - geolocator_apple (1.2.0): - Flutter - google_maps_flutter_ios (0.0.1): @@ -65,7 +62,7 @@ PODS: - GoogleMaps (< 9.0) - google_sign_in_ios (0.0.1): - Flutter - - GoogleSignIn (~> 6.2) + - GoogleSignIn (~> 7.0) - GoogleAppMeasurement (10.23.0): - GoogleAppMeasurement/AdIdSupport (= 10.23.0) - GoogleUtilities/AppDelegateSwizzler (~> 7.11) @@ -73,14 +70,14 @@ PODS: - GoogleUtilities/Network (~> 7.11) - "GoogleUtilities/NSData+zlib (~> 7.11)" - nanopb (< 2.30911.0, >= 2.30908.0) - - GoogleAppMeasurement/AdIdSupport (10.23.0): - - GoogleAppMeasurement/WithoutAdIdSupport (= 10.23.0) + - GoogleAppMeasurement/AdIdSupport (10.24.0): + - GoogleAppMeasurement/WithoutAdIdSupport (= 10.24.0) - GoogleUtilities/AppDelegateSwizzler (~> 7.11) - GoogleUtilities/MethodSwizzler (~> 7.11) - GoogleUtilities/Network (~> 7.11) - "GoogleUtilities/NSData+zlib (~> 7.11)" - nanopb (< 2.30911.0, >= 2.30908.0) - - GoogleAppMeasurement/WithoutAdIdSupport (10.23.0): + - GoogleAppMeasurement/WithoutAdIdSupport (10.24.0): - GoogleUtilities/AppDelegateSwizzler (~> 7.11) - GoogleUtilities/MethodSwizzler (~> 7.11) - GoogleUtilities/Network (~> 7.11) @@ -91,10 +88,10 @@ PODS: - GoogleMaps/Base (7.4.0) - GoogleMaps/Maps (7.4.0): - GoogleMaps/Base - - GoogleSignIn (6.2.4): + - GoogleSignIn (7.0.0): - AppAuth (~> 1.5) - - GTMAppAuth (~> 1.3) - - GTMSessionFetcher/Core (< 3.0, >= 1.1) + - GTMAppAuth (< 3.0, >= 1.3) + - GTMSessionFetcher/Core (< 4.0, >= 1.1) - GoogleUtilities/AppDelegateSwizzler (7.13.0): - GoogleUtilities/Environment - GoogleUtilities/Logger @@ -123,10 +120,10 @@ PODS: - GoogleUtilities/UserDefaults (7.13.0): - GoogleUtilities/Logger - GoogleUtilities/Privacy - - GTMAppAuth (1.3.1): + - GTMAppAuth (2.0.0): - AppAuth/Core (~> 1.6) - - GTMSessionFetcher/Core (< 3.0, >= 1.5) - - GTMSessionFetcher/Core (2.3.0) + - GTMSessionFetcher/Core (< 4.0, >= 1.5) + - GTMSessionFetcher/Core (3.3.2) - location (0.0.1): - Flutter - nanopb (2.30910.0): @@ -137,14 +134,14 @@ PODS: - path_provider_foundation (0.0.1): - Flutter - FlutterMacOS - - permission_handler_apple (9.1.1): + - permission_handler_apple (9.3.0): - Flutter - platform_device_id (0.0.1): - Flutter - PromisesObjC (2.4.0) - sqflite (0.0.3): - Flutter - - FMDB (>= 2.7.5) + - FlutterMacOS - Toast (4.1.0) - url_launcher_ios (0.0.1): - Flutter @@ -160,12 +157,12 @@ DEPENDENCIES: - fluttertoast (from `.symlinks/plugins/fluttertoast/ios`) - geolocator_apple (from `.symlinks/plugins/geolocator_apple/ios`) - google_maps_flutter_ios (from `.symlinks/plugins/google_maps_flutter_ios/ios`) - - google_sign_in_ios (from `.symlinks/plugins/google_sign_in_ios/ios`) + - google_sign_in_ios (from `.symlinks/plugins/google_sign_in_ios/darwin`) - location (from `.symlinks/plugins/location/ios`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) - platform_device_id (from `.symlinks/plugins/platform_device_id/ios`) - - sqflite (from `.symlinks/plugins/sqflite/ios`) + - sqflite (from `.symlinks/plugins/sqflite/darwin`) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) SPEC REPOS: @@ -176,7 +173,6 @@ SPEC REPOS: - FirebaseCore - FirebaseCoreInternal - FirebaseInstallations - - FMDB - GoogleAppMeasurement - GoogleMaps - GoogleSignIn @@ -205,7 +201,7 @@ EXTERNAL SOURCES: google_maps_flutter_ios: :path: ".symlinks/plugins/google_maps_flutter_ios/ios" google_sign_in_ios: - :path: ".symlinks/plugins/google_sign_in_ios/ios" + :path: ".symlinks/plugins/google_sign_in_ios/darwin" location: :path: ".symlinks/plugins/location/ios" path_provider_foundation: @@ -215,42 +211,41 @@ EXTERNAL SOURCES: platform_device_id: :path: ".symlinks/plugins/platform_device_id/ios" sqflite: - :path: ".symlinks/plugins/sqflite/ios" + :path: ".symlinks/plugins/sqflite/darwin" url_launcher_ios: :path: ".symlinks/plugins/url_launcher_ios/ios" SPEC CHECKSUMS: - AppAuth: a13994980c1ec792f7e2e665acd4d4aa6be43240 + AppAuth: 182c5b88630569df5acb672720534756c29b3358 device_info: d7d233b645a32c40dfdc212de5cf646ca482f175 device_info_plus: c6fb39579d0f423935b0c9ce7ee2f44b71b9fce6 - Firebase: 333ec7c6b12fa09c77b5162cda6b862102211d50 - FirebaseAnalytics: 45f6e2e5ef8ccbb90be73ae983c3b20fa78837f7 - FirebaseCore: 63efb128decaebb04c4033ed4e05fb0bb1cccef1 - FirebaseCoreInternal: 6a292e6f0bece1243a737e81556e56e5e19282e3 - FirebaseInstallations: 42d6ead4605d6eafb3b6683674e80e18eb6f2c35 - Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 + Firebase: 91fefd38712feb9186ea8996af6cbdef41473442 + FirebaseAnalytics: b5efc493eb0f40ec560b04a472e3e1a15d39ca13 + FirebaseCore: 11dc8a16dfb7c5e3c3f45ba0e191a33ac4f50894 + FirebaseCoreInternal: bcb5acffd4ea05e12a783ecf835f2210ce3dc6af + FirebaseInstallations: 8f581fca6478a50705d2bd2abd66d306e0f5736e + Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 flutter_config: f48f0d47a284f1791aacce2687eabb3309ba7a41 flutter_secure_storage: 23fc622d89d073675f2eaa109381aefbcf5a49be fluttertoast: 31b00dabfa7fb7bacd9e7dbee580d7a2ff4bf265 - FMDB: aa44149f6fb634b1ac54f64f47064bb0d0c5a032 - geolocator_apple: cc556e6844d508c95df1e87e3ea6fa4e58c50401 + geolocator_apple: 9157311f654584b9bb72686c55fc02a97b73f461 google_maps_flutter_ios: d1318b4ff711612cab16862d7a87e31a7403d458 - google_sign_in_ios: 1256ff9d941db546373826966720b0c24804bcdd + google_sign_in_ios: 8115e3fbe097e6509beb819ed602d47369d9011f GoogleAppMeasurement: 453eb0de99fcf2bdec9403e9ac5d7871fdba3e3f GoogleMaps: 032f676450ba0779bd8ce16840690915f84e57ac - GoogleSignIn: 5651ce3a61e56ca864160e79b484cd9ed3f49b7a + GoogleSignIn: b232380cf495a429b8095d3178a8d5855b42e842 GoogleUtilities: d053d902a8edaa9904e1bd00c37535385b8ed152 - GTMAppAuth: 0ff230db599948a9ad7470ca667337803b3fc4dd - GTMSessionFetcher: 3a63d75eecd6aa32c2fc79f578064e1214dfdec2 + GTMAppAuth: 99fb010047ba3973b7026e45393f51f27ab965ae + GTMSessionFetcher: 0e876eea9782ec6462e91ab872711c357322c94f location: d5cf8598915965547c3f36761ae9cc4f4e87d22e nanopb: 438bc412db1928dac798aa6fd75726007be04262 - path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943 + path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c permission_handler_apple: e76247795d700c14ea09e3a2d8855d41ee80a2e6 platform_device_id: 81b3e2993881f87d0c82ef151dc274df4869aef5 PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 - sqflite: 31f7eba61e3074736dff8807a9b41581e4f7f15a + sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec Toast: ec33c32b8688982cecc6348adeae667c1b9938da - url_launcher_ios: 68d46cc9766d0c41dbdc884310529557e3cd7a86 + url_launcher_ios: bbd758c6e7f9fd7b5b1d4cde34d2b95fcce5e812 PODFILE CHECKSUM: 025aa7201e4cab1f9c884abb0535b2c942d9df36 diff --git a/game/ios/Runner.xcodeproj/project.pbxproj b/game/ios/Runner.xcodeproj/project.pbxproj index c7300787..6bc91567 100644 --- a/game/ios/Runner.xcodeproj/project.pbxproj +++ b/game/ios/Runner.xcodeproj/project.pbxproj @@ -159,7 +159,7 @@ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 1430; + LastUpgradeCheck = 1510; ORGANIZATIONNAME = ""; TargetAttributes = { 97C146ED1CF9000F007C117D = { diff --git a/game/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/game/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index b52b2e69..e67b2808 100644 --- a/game/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/game/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ 0 ? tasksFinished / totalTasks : 0) * + constraints.maxWidth, + height: 13, + alignment: Alignment.centerLeft, + child: Container( + decoration: new BoxDecoration( + color: Color.fromARGB(197, 237, 86, 86), + shape: BoxShape.rectangle, + borderRadius: BorderRadius.all(Radius.circular(16.0)), + ), + ), + ), + Container( + height: 3, + width: (totalTasks > 0 ? tasksFinished / totalTasks : 0) * + constraints.maxWidth - + 16, + margin: EdgeInsets.only(left: 8, top: 3), + alignment: Alignment.centerLeft, + decoration: new BoxDecoration( + color: Color(0x99F3C6C6), + shape: BoxShape.rectangle, + borderRadius: BorderRadius.all(Radius.circular(5.0)), + ), + ), + ]); + })), + Padding( + padding: const EdgeInsets.only(left: 8.0), + child: Text( + tasksFinished.toString() + "/" + totalTasks.toString(), + ), + ), + ], + ); + } +} + +class AchievementCell extends StatefulWidget { + final SvgPicture thumbnail; + final String description; + final int tasksFinished; + final int totalTasks; + + const AchievementCell( + this.description, this.thumbnail, this.tasksFinished, this.totalTasks, + {Key? key}) + : super(key: key); + + @override + State createState() => + _AchievementCellState(description, thumbnail, tasksFinished, totalTasks); +} + +class _AchievementCellState extends State { + final String description; + final SvgPicture thumbnail; + final int tasksFinished; + final int totalTasks; + // newly added field + // final int totalDistance; + + _AchievementCellState( + this.description, this.thumbnail, this.tasksFinished, this.totalTasks + // newly added field + // this.totalDistance + ); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: () async {}, + child: Container( + padding: EdgeInsets.all(5), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(15), + boxShadow: [ + BoxShadow( + color: Color.fromARGB(255, 198, 198, 198), + blurRadius: 2, + offset: Offset(0, 4), + ), + ], + ), + child: Container( + margin: EdgeInsets.all(10), + height: 64, + child: Row( + children: [ + Container(margin: EdgeInsets.only(right: 12), child: thumbnail), + Column( + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Align( + alignment: Alignment.centerLeft, + child: Text( + description, + style: TextStyle( + color: Color.fromARGB(204, 0, 0, 0), + fontSize: 14, + fontFamily: 'Poppins', + fontWeight: FontWeight.w500, + ), + ), + ), + Spacer(), + Align( + alignment: Alignment.bottomCenter, + child: LoadingBar(3, 4)), + ], + ) + ], + ), + ), + ), + ); + } +} diff --git a/game/lib/achievements/achievements_page.dart b/game/lib/achievements/achievements_page.dart new file mode 100644 index 00000000..eb3f75ad --- /dev/null +++ b/game/lib/achievements/achievements_page.dart @@ -0,0 +1,205 @@ +import 'dart:io'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:game/achievements/achievement_cell.dart'; +import 'package:game/api/game_api.dart'; +import 'package:game/api/game_client_dto.dart'; +import 'package:game/model/challenge_model.dart'; +import 'package:game/model/event_model.dart'; +import 'package:game/model/group_model.dart'; +import 'package:game/utils/utility_functions.dart'; +import 'package:game/model/tracker_model.dart'; +import 'package:provider/provider.dart'; + +class AchievementCellDto { + AchievementCellDto({ + required this.location, + required this.name, + required this.lat, + required this.long, + required this.thumbnail, + required this.complete, + required this.description, + required this.difficulty, + required this.points, + required this.eventId, + }); + late String location; + late String name; + late double? lat; + late double? long; + late SvgPicture thumbnail; + late bool complete; + late String description; + late String difficulty; + late int points; + late String eventId; +} + +class AchievementsPage extends StatefulWidget { + const AchievementsPage({super.key}); + + @override + State createState() => _AchievementsPageState(); +} + +class _AchievementsPageState extends State { + List selectedCategories = []; + List selectedLocations = []; + String selectedDifficulty = ''; + + List eventData = []; + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + color: Color(0xFFED5656), + ), + child: SafeArea( + bottom: false, + child: Scaffold( + appBar: AppBar( + title: const Text('Achievements'), + ), + body: Container( + width: double.infinity, + height: double.infinity, + decoration: BoxDecoration( + color: Color.fromARGB(255, 255, 248, 241), + ), + child: Padding( + padding: EdgeInsets.all(30), + child: Column( + children: [ + Expanded(child: Consumer5( + builder: (context, + myEventModel, + groupModel, + trackerModel, + challengeModel, + apiClient, + child) { + if (myEventModel.searchResults == null) { + myEventModel.searchEvents( + 0, + 1000, + [ + EventTimeLimitationDto.PERPETUAL, + EventTimeLimitationDto.LIMITED_TIME + ], + false, + false, + false); + } + final events = myEventModel.searchResults ?? []; + if (!events.any((element) => + element.id == groupModel.curEventId)) { + final curEvent = myEventModel + .getEventById(groupModel.curEventId ?? ""); + if (curEvent != null) events.add(curEvent); + } + eventData.clear(); + + for (EventDto event in events) { + var tracker = + trackerModel.trackerByEventId(event.id); + var numberCompleted = + tracker?.prevChallenges.length ?? 0; + var complete = + (numberCompleted == event.challenges?.length); + var locationCount = event.challenges?.length ?? 0; + DateTime now = DateTime.now(); + DateTime endtime = + HttpDate.parse(event.endTime ?? ""); + + Duration timeTillExpire = endtime.difference(now); + if (locationCount != 1) continue; + var challenge = challengeModel + .getChallengeById(event.challenges?[0] ?? ""); + + // print("Doing Event with now/endtime " + event.description.toString() + now.toString() + "/" + endtime.toString()); + if (challenge == null) { + // print("Challenge is null for event " + event.description.toString()); + + continue; + } + final challengeLocation = + challenge.location?.name ?? ""; + + bool eventMatchesDifficultySelection; + bool eventMatchesCategorySelection; + bool eventMatchesLocationSelection; + + if (selectedDifficulty.length == 0 || + selectedDifficulty == event.difficulty?.name) + eventMatchesDifficultySelection = true; + else + eventMatchesDifficultySelection = false; + + if (selectedLocations.length > 0) { + if (selectedLocations.contains(challengeLocation)) + eventMatchesLocationSelection = true; + else + eventMatchesLocationSelection = false; + } else + eventMatchesLocationSelection = true; + + if (selectedCategories.length > 0) { + if (selectedCategories + .contains(event.category?.name)) + eventMatchesCategorySelection = true; + else + eventMatchesCategorySelection = false; + } else + eventMatchesCategorySelection = true; + if (!complete && + !timeTillExpire.isNegative && + eventMatchesDifficultySelection && + eventMatchesCategorySelection && + eventMatchesLocationSelection) { + eventData.add(AchievementCellDto( + location: + friendlyLocation[challenge.location] ?? "", + name: event.name ?? "", + lat: challenge.latF ?? null, + long: challenge.longF ?? null, + thumbnail: SvgPicture.asset( + "assets/icons/achievementsilver.svg"), + complete: complete, + description: event.description ?? "", + difficulty: + friendlyDifficulty[event.difficulty] ?? "", + points: challenge.points ?? 0, + eventId: event.id, + )); + } else if (event.id == groupModel.curEventId) { + apiClient.serverApi?.setCurrentEvent( + SetCurrentEventDto(eventId: "")); + } + } + + return ListView.separated( + padding: const EdgeInsets.symmetric(horizontal: 3), + itemCount: eventData.length, + itemBuilder: (context, index) { + return AchievementCell( + key: UniqueKey(), + eventData[index].description, + eventData[index].thumbnail, + 3, + 4); + }, + physics: BouncingScrollPhysics(), + separatorBuilder: (context, index) { + return SizedBox(height: 10); + }, + ); + })) + ], + ), + )), + ))); + } +} diff --git a/game/lib/api/game_api.dart b/game/lib/api/game_api.dart index e60a4a3f..89171828 100644 --- a/game/lib/api/game_api.dart +++ b/game/lib/api/game_api.dart @@ -3,7 +3,9 @@ import 'dart:io'; import 'package:flutter/cupertino.dart'; import 'package:game/api/game_client_api.dart'; +import 'package:game/api/game_client_dto.dart'; import 'package:game/api/game_server_api.dart'; +import 'package:game/utils/utility_functions.dart'; import 'package:google_sign_in/google_sign_in.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:socket_io_client/socket_io_client.dart' as IO; @@ -55,6 +57,7 @@ class ApiClient extends ChangeNotifier { IO.OptionBuilder() .setTransports(["websocket"]) .disableAutoConnect() + .disableReconnection() .setAuth({'token': _accessToken}) .build()); @@ -62,6 +65,7 @@ class ApiClient extends ChangeNotifier { _serverApi = null; notifyListeners(); }); + socket.onConnect((data) { _socket = socket; @@ -74,6 +78,7 @@ class ApiClient extends ChangeNotifier { notifyListeners(); }); + socket.onConnectError((data) { connectionFailed = true; notifyListeners(); @@ -88,7 +93,6 @@ class ApiClient extends ChangeNotifier { _createSocket(true); } else { authenticated = false; - _clientApi.disconnectedController.add(null); _socket?.dispose(); _socket = null; notifyListeners(); @@ -123,59 +127,105 @@ class ApiClient extends ChangeNotifier { _refreshToken = token; final access = await _refreshAccess(true); authenticated = access; - if (!access) { - _clientApi.disconnectedController.add(null); - } notifyListeners(); return access; } authenticated = false; - _clientApi.disconnectedController.add(null); notifyListeners(); return false; } - Future connect(String idToken, Uri url, String enrollmentType, - String year, String username) async { + Future connectDevice( + String year, + LoginEnrollmentTypeDto enrollmentType, + String username, + String college, + String major, + List interests) async { + final String? id = await getId(); + return connect(id!, _deviceLoginUrl, year, enrollmentType, username, + college, major, interests, + noRegister: false); + } + + Future connectGoogle( + GoogleSignInAccount gAccount, + String year, + LoginEnrollmentTypeDto enrollmentType, + String username, + String college, + String major, + List interests) async { + final auth = await gAccount.authentication; + return connect(auth.idToken ?? "", _googleLoginUrl, year, enrollmentType, + username, college, major, interests, + noRegister: false); + } + + Future connectGoogleNoRegister( + GoogleSignInAccount gAccount) async { + final auth = await gAccount.authentication; + return connect(auth.idToken ?? "", _googleLoginUrl, "", + LoginEnrollmentTypeDto.GUEST, "", "", "", [], + noRegister: true); + } + + Future connect( + String idToken, + Uri url, + String year, + LoginEnrollmentTypeDto enrollmentType, + String username, + String college, + String major, + List interests, + {bool noRegister = false}) async { final pos = await GeoPoint.current(); - if (true) { - final loginResponse = await http.post(url, - headers: { - 'Content-Type': 'application/json; charset=UTF-8', - }, - body: jsonEncode({ - "idToken": idToken, - "lat": pos?.lat.toString() ?? "0", - "enrollmentType": enrollmentType, - "year": year, - "username": username, - "long": pos?.long.toString() ?? "0", - "aud": Platform.isIOS ? "ios" : "android" - })); - - if (loginResponse.statusCode == 201 && loginResponse.body != "") { - final responseBody = jsonDecode(loginResponse.body); - this._accessToken = responseBody["accessToken"]; - this._refreshToken = responseBody["refreshToken"]; - await _saveToken(); - _createSocket(false); - return loginResponse; - } else { - print(loginResponse.body); - } - authenticated = false; - _clientApi.disconnectedController.add(null); - notifyListeners(); - print("Failed to connect to server!"); - return null; + final loginDto = LoginDto( + idToken: idToken, + latF: pos.lat, + enrollmentType: enrollmentType, + year: year, + username: username, + college: college, + major: major, + interests: interests.join(","), + longF: pos.long, + aud: Platform.isIOS ? LoginAudDto.ios : LoginAudDto.android, + noRegister: noRegister); + /* + if (post != null) { */ + final loginResponse = await http.post(url, + headers: { + 'Content-Type': 'application/json; charset=UTF-8', + }, + body: jsonEncode(loginDto.toJson())); + + if (loginResponse.statusCode == 201 && loginResponse.body != "") { + final responseBody = jsonDecode(loginResponse.body); + this._accessToken = responseBody["accessToken"]; + this._refreshToken = responseBody["refreshToken"]; + await _saveToken(); + _createSocket(false); + return loginResponse; + } else { + print(loginResponse.body); + } + authenticated = false; + notifyListeners(); + + print("Failed to connect to server!"); + return null; + /* } print("Failed to get location data!"); return null; + */ } - Future connectGoogle() async { + Future signinGoogle() async { final account = await _googleSignIn.signIn(); return account; } @@ -191,7 +241,6 @@ class ApiClient extends ChangeNotifier { _socket = null; authenticated = false; - _clientApi.disconnectedController.add(null); notifyListeners(); } } diff --git a/game/lib/api/game_client_api.dart b/game/lib/api/game_client_api.dart index 15b4a686..ae08a79e 100644 --- a/game/lib/api/game_client_api.dart +++ b/game/lib/api/game_client_api.dart @@ -63,22 +63,27 @@ class GameClientApi { Stream get updateOrganizationDataStream => _updateOrganizationDataController.stream; - final _reconnectedController = StreamController.broadcast(sync: true); - Stream get reconnectedStream => _reconnectedController.stream; + final _updateLeaderPositionController = + StreamController.broadcast(sync: true); + Stream get updateLeaderPositionStream => + _updateLeaderPositionController.stream; - final _reconnectingController = StreamController.broadcast(sync: true); - Stream get reconnectingStream => _reconnectingController.stream; + final _reconnectedController = StreamController.broadcast(sync: true); + Stream get reconnectedStream => _reconnectedController.stream; - final _connectedController = StreamController.broadcast(sync: true); - Stream get connectedStream => _connectedController.stream; + final _reconnectingController = StreamController.broadcast(sync: true); + Stream get reconnectingStream => _reconnectingController.stream; - final disconnectedController = StreamController.broadcast(sync: true); - Stream get disconnectedStream => disconnectedController.stream; + final _connectedController = StreamController.broadcast(sync: true); + Stream get connectedStream => _connectedController.stream; + + final disconnectedController = StreamController.broadcast(sync: true); + Stream get disconnectedStream => disconnectedController.stream; void connectSocket(Socket sock) { - sock.onReconnect((data) => _reconnectingController.add(null)); - sock.onReconnecting((data) => _reconnectedController.add(null)); - sock.onDisconnect((data) => disconnectedController.add(null)); + sock.onReconnect((data) => _reconnectedController.add(true)); + sock.onReconnecting((data) => _reconnectingController.add(true)); + sock.onDisconnect((data) => disconnectedController.add(true)); sock.on( "updateUserData", @@ -135,7 +140,12 @@ class GameClientApi { (data) => _updateOrganizationDataController .add(UpdateOrganizationDataDto.fromJson(data))); - _connectedController.add(null); + sock.on( + "updateLeaderPosition", + (data) => _updateLeaderPositionController + .add(UpdateLeaderPositionDto.fromJson(data))); + + _connectedController.add(true); } GameClientApi() {} diff --git a/game/lib/api/game_client_dto.dart b/game/lib/api/game_client_dto.dart index 1a73ac1e..059a88fc 100644 --- a/game/lib/api/game_client_dto.dart +++ b/game/lib/api/game_client_dto.dart @@ -208,7 +208,7 @@ class AchievementTrackerDto { class UpdateAchievementDataDto { Map toJson() { Map fields = {}; - fields['achievement'] = achievement!.toJson(); + fields['achievement'] = achievement.toJson(); fields['deleted'] = deleted; return fields; } @@ -258,27 +258,45 @@ class LoginDto { Map toJson() { Map fields = {}; fields['idToken'] = idToken; - fields['lat'] = lat; - fields['long'] = long; + fields['noRegister'] = noRegister; + if (latF != null) { + fields['latF'] = latF; + } + if (longF != null) { + fields['longF'] = longF; + } if (username != null) { fields['username'] = username; } if (year != null) { fields['year'] = year; } + if (college != null) { + fields['college'] = college; + } + if (major != null) { + fields['major'] = major; + } + if (interests != null) { + fields['interests'] = interests; + } if (aud != null) { fields['aud'] = aud!.name; } - fields['enrollmentType'] = enrollmentType!.name; + fields['enrollmentType'] = enrollmentType.name; return fields; } LoginDto.fromJson(Map fields) { idToken = fields["idToken"]; - lat = fields["lat"]; - long = fields["long"]; + noRegister = fields["noRegister"]; + latF = fields.containsKey('latF') ? (fields["latF"]!.toDouble()) : null; + longF = fields.containsKey('longF') ? (fields["longF"]!.toDouble()) : null; username = fields.containsKey('username') ? (fields["username"]) : null; year = fields.containsKey('year') ? (fields["year"]) : null; + college = fields.containsKey('college') ? (fields["college"]) : null; + major = fields.containsKey('major') ? (fields["major"]) : null; + interests = fields.containsKey('interests') ? (fields["interests"]) : null; aud = fields.containsKey('aud') ? (LoginAudDto.values.byName(fields['aud'])) : null; @@ -288,29 +306,41 @@ class LoginDto { void partialUpdate(LoginDto other) { idToken = other.idToken; - lat = other.lat; - long = other.long; + noRegister = other.noRegister; + latF = other.latF == null ? latF : other.latF; + longF = other.longF == null ? longF : other.longF; username = other.username == null ? username : other.username; year = other.year == null ? year : other.year; + college = other.college == null ? college : other.college; + major = other.major == null ? major : other.major; + interests = other.interests == null ? interests : other.interests; aud = other.aud == null ? aud : other.aud; enrollmentType = other.enrollmentType; } LoginDto({ required this.idToken, - required this.lat, - required this.long, + required this.noRegister, + this.latF, + this.longF, this.username, this.year, + this.college, + this.major, + this.interests, this.aud, required this.enrollmentType, }); late String idToken; - late int lat; - late int long; + late bool noRegister; + late double? latF; + late double? longF; late String? username; late String? year; + late String? college; + late String? major; + late String? interests; late LoginAudDto? aud; late LoginEnrollmentTypeDto enrollmentType; } @@ -487,7 +517,7 @@ class RequestChallengeDataDto { class UpdateChallengeDataDto { Map toJson() { Map fields = {}; - fields['challenge'] = challenge!.toJson(); + fields['challenge'] = challenge.toJson(); fields['deleted'] = deleted; return fields; } @@ -555,74 +585,6 @@ class SetCurrentChallengeDto { late String challengeId; } -class LeaderDto { - Map toJson() { - Map fields = {}; - fields['userId'] = userId; - fields['username'] = username; - fields['score'] = score; - return fields; - } - - LeaderDto.fromJson(Map fields) { - userId = fields["userId"]; - username = fields["username"]; - score = fields["score"]; - } - - void partialUpdate(LeaderDto other) { - userId = other.userId; - username = other.username; - score = other.score; - } - - LeaderDto({ - required this.userId, - required this.username, - required this.score, - }); - - late String userId; - late String username; - late int score; -} - -class UpdateLeaderDataDto { - Map toJson() { - Map fields = {}; - fields['eventId'] = eventId; - fields['offset'] = offset; - fields['users'] = users! - .map>((dynamic val) => val!.toJson()) - .toList(); - return fields; - } - - UpdateLeaderDataDto.fromJson(Map fields) { - eventId = fields["eventId"]; - offset = fields["offset"]; - users = fields["users"] - .map((dynamic val) => LeaderDto.fromJson(val)) - .toList(); - } - - void partialUpdate(UpdateLeaderDataDto other) { - eventId = other.eventId; - offset = other.offset; - users = other.users; - } - - UpdateLeaderDataDto({ - required this.eventId, - required this.offset, - required this.users, - }); - - late String eventId; - late int offset; - late List users; -} - class UpdateErrorDto { Map toJson() { Map fields = {}; @@ -650,31 +612,41 @@ class UpdateErrorDto { late String message; } -class RequestAllEventDataDto { +class RequestFilteredEventsDto { Map toJson() { Map fields = {}; - fields['offset'] = offset; - fields['count'] = count; + fields['difficulty'] = difficulty; + fields['location'] = location; + fields['category'] = category; + fields['filterId'] = filterId; return fields; } - RequestAllEventDataDto.fromJson(Map fields) { - offset = fields["offset"]; - count = fields["count"]; + RequestFilteredEventsDto.fromJson(Map fields) { + difficulty = List.from(fields['difficulty']); + location = List.from(fields['location']); + category = List.from(fields['category']); + filterId = List.from(fields['filterId']); } - void partialUpdate(RequestAllEventDataDto other) { - offset = other.offset; - count = other.count; + void partialUpdate(RequestFilteredEventsDto other) { + difficulty = other.difficulty; + location = other.location; + category = other.category; + filterId = other.filterId; } - RequestAllEventDataDto({ - required this.offset, - required this.count, + RequestFilteredEventsDto({ + required this.difficulty, + required this.location, + required this.category, + required this.filterId, }); - late int offset; - late int count; + late List difficulty; + late List location; + late List category; + late List filterId; } class RequestEventDataDto { @@ -708,30 +680,139 @@ class RequestEventLeaderDataDto { Map fields = {}; fields['offset'] = offset; fields['count'] = count; - fields['eventId'] = eventId; + if (eventId != null) { + fields['eventId'] = eventId; + } return fields; } RequestEventLeaderDataDto.fromJson(Map fields) { offset = fields["offset"]; count = fields["count"]; - eventId = fields["eventId"]; + eventId = fields.containsKey('eventId') ? (fields["eventId"]) : null; } void partialUpdate(RequestEventLeaderDataDto other) { offset = other.offset; count = other.count; - eventId = other.eventId; + eventId = other.eventId == null ? eventId : other.eventId; } RequestEventLeaderDataDto({ required this.offset, required this.count, - required this.eventId, + this.eventId, }); late int offset; late int count; + late String? eventId; +} + +class LeaderDto { + Map toJson() { + Map fields = {}; + fields['userId'] = userId; + fields['username'] = username; + fields['score'] = score; + return fields; + } + + LeaderDto.fromJson(Map fields) { + userId = fields["userId"]; + username = fields["username"]; + score = fields["score"]; + } + + void partialUpdate(LeaderDto other) { + userId = other.userId; + username = other.username; + score = other.score; + } + + LeaderDto({ + required this.userId, + required this.username, + required this.score, + }); + + late String userId; + late String username; + late int score; +} + +class UpdateLeaderDataDto { + Map toJson() { + Map fields = {}; + if (eventId != null) { + fields['eventId'] = eventId; + } + fields['offset'] = offset; + fields['users'] = users + .map>((dynamic val) => val!.toJson()) + .toList(); + return fields; + } + + UpdateLeaderDataDto.fromJson(Map fields) { + eventId = fields.containsKey('eventId') ? (fields["eventId"]) : null; + offset = fields["offset"]; + users = fields["users"] + .map((dynamic val) => LeaderDto.fromJson(val)) + .toList(); + } + + void partialUpdate(UpdateLeaderDataDto other) { + eventId = other.eventId == null ? eventId : other.eventId; + offset = other.offset; + users = other.users; + } + + UpdateLeaderDataDto({ + this.eventId, + required this.offset, + required this.users, + }); + + late String? eventId; + late int offset; + late List users; +} + +class UpdateLeaderPositionDto { + Map toJson() { + Map fields = {}; + fields['playerId'] = playerId; + fields['newTotalScore'] = newTotalScore; + fields['newEventScore'] = newEventScore; + fields['eventId'] = eventId; + return fields; + } + + UpdateLeaderPositionDto.fromJson(Map fields) { + playerId = fields["playerId"]; + newTotalScore = fields["newTotalScore"]; + newEventScore = fields["newEventScore"]; + eventId = fields["eventId"]; + } + + void partialUpdate(UpdateLeaderPositionDto other) { + playerId = other.playerId; + newTotalScore = other.newTotalScore; + newEventScore = other.newEventScore; + eventId = other.eventId; + } + + UpdateLeaderPositionDto({ + required this.playerId, + required this.newTotalScore, + required this.newEventScore, + required this.eventId, + }); + + late String playerId; + late int newTotalScore; + late int newEventScore; late String eventId; } @@ -945,7 +1026,7 @@ class EventTrackerDto { fields['isRanked'] = isRanked; fields['hintsUsed'] = hintsUsed; fields['curChallengeId'] = curChallengeId; - fields['prevChallenges'] = prevChallenges! + fields['prevChallenges'] = prevChallenges .map>((dynamic val) => val!.toJson()) .toList(); return fields; @@ -987,7 +1068,7 @@ class EventTrackerDto { class UpdateEventTrackerDataDto { Map toJson() { Map fields = {}; - fields['tracker'] = tracker!.toJson(); + fields['tracker'] = tracker.toJson(); return fields; } @@ -1009,7 +1090,7 @@ class UpdateEventTrackerDataDto { class UpdateEventDataDto { Map toJson() { Map fields = {}; - fields['event'] = event!.toJson(); + fields['event'] = event.toJson(); fields['deleted'] = deleted; return fields; } @@ -1214,7 +1295,7 @@ class GroupDto { class UpdateGroupDataDto { Map toJson() { Map fields = {}; - fields['group'] = group!.toJson(); + fields['group'] = group.toJson(); fields['deleted'] = deleted; return fields; } @@ -1376,7 +1457,7 @@ class RequestOrganizationDataDto { class UpdateOrganizationDataDto { Map toJson() { Map fields = {}; - fields['organization'] = organization!.toJson(); + fields['organization'] = organization.toJson(); fields['deleted'] = deleted; return fields; } @@ -1413,72 +1494,6 @@ class CloseAccountDto { CloseAccountDto(); } -class SetUsernameDto { - Map toJson() { - Map fields = {}; - fields['newUsername'] = newUsername; - return fields; - } - - SetUsernameDto.fromJson(Map fields) { - newUsername = fields["newUsername"]; - } - - void partialUpdate(SetUsernameDto other) { - newUsername = other.newUsername; - } - - SetUsernameDto({ - required this.newUsername, - }); - - late String newUsername; -} - -class SetMajorDto { - Map toJson() { - Map fields = {}; - fields['newMajor'] = newMajor; - return fields; - } - - SetMajorDto.fromJson(Map fields) { - newMajor = fields["newMajor"]; - } - - void partialUpdate(SetMajorDto other) { - newMajor = other.newMajor; - } - - SetMajorDto({ - required this.newMajor, - }); - - late String newMajor; -} - -class SetGraduationYearDto { - Map toJson() { - Map fields = {}; - fields['newYear'] = newYear; - return fields; - } - - SetGraduationYearDto.fromJson(Map fields) { - newYear = fields["newYear"]; - } - - void partialUpdate(SetGraduationYearDto other) { - newYear = other.newYear; - } - - SetGraduationYearDto({ - required this.newYear, - }); - - late String newYear; -} - class BanUserDto { Map toJson() { Map fields = {}; @@ -1509,7 +1524,7 @@ class BanUserDto { class SetAuthToOAuthDto { Map toJson() { Map fields = {}; - fields['provider'] = provider!.name; + fields['provider'] = provider.name; fields['authId'] = authId; return fields; } @@ -1662,6 +1677,15 @@ class UserDto { if (year != null) { fields['year'] = year; } + if (college != null) { + fields['college'] = college; + } + if (major != null) { + fields['major'] = major; + } + if (interests != null) { + fields['interests'] = interests; + } if (score != null) { fields['score'] = score; } @@ -1691,6 +1715,11 @@ class UserDto { : null; email = fields.containsKey('email') ? (fields["email"]) : null; year = fields.containsKey('year') ? (fields["year"]) : null; + college = fields.containsKey('college') ? (fields["college"]) : null; + major = fields.containsKey('major') ? (fields["major"]) : null; + interests = fields.containsKey('interests') + ? (List.from(fields['interests'])) + : null; score = fields.containsKey('score') ? (fields["score"]) : null; isBanned = fields.containsKey('isBanned') ? (fields["isBanned"]) : null; groupId = fields.containsKey('groupId') ? (fields["groupId"]) : null; @@ -1712,6 +1741,9 @@ class UserDto { other.enrollmentType == null ? enrollmentType : other.enrollmentType; email = other.email == null ? email : other.email; year = other.year == null ? year : other.year; + college = other.college == null ? college : other.college; + major = other.major == null ? major : other.major; + interests = other.interests == null ? interests : other.interests; score = other.score == null ? score : other.score; isBanned = other.isBanned == null ? isBanned : other.isBanned; groupId = other.groupId == null ? groupId : other.groupId; @@ -1727,6 +1759,9 @@ class UserDto { this.enrollmentType, this.email, this.year, + this.college, + this.major, + this.interests, this.score, this.isBanned, this.groupId, @@ -1740,6 +1775,9 @@ class UserDto { late UserEnrollmentTypeDto? enrollmentType; late String? email; late String? year; + late String? college; + late String? major; + late List? interests; late int? score; late bool? isBanned; late String? groupId; @@ -1751,7 +1789,7 @@ class UserDto { class UpdateUserDataDto { Map toJson() { Map fields = {}; - fields['user'] = user!.toJson(); + fields['user'] = user.toJson(); fields['deleted'] = deleted; return fields; } diff --git a/game/lib/api/game_server_api.dart b/game/lib/api/game_server_api.dart index da860496..b9c7a934 100644 --- a/game/lib/api/game_server_api.dart +++ b/game/lib/api/game_server_api.dart @@ -3,7 +3,6 @@ // OTHERWISE YOUR CHANGES MAY BE OVERWRITTEN! import 'dart:async'; -import 'dart:convert'; import 'package:game/api/game_client_dto.dart'; import 'package:socket_io_client/socket_io_client.dart'; @@ -36,7 +35,7 @@ class GameServerApi { void _invokeWithRefresh(String ev, Map data) { _refreshEv = ev; _refreshDat = data; - print(ev); + //print(ev); _socket.emit(ev, data); } @@ -52,15 +51,15 @@ class GameServerApi { void completedChallenge(CompletedChallengeDto dto) => _invokeWithRefresh("completedChallenge", dto.toJson()); - void requestGlobalLeaderData(RequestGlobalLeaderDataDto dto) => - _invokeWithRefresh("requestGlobalLeaderData", dto.toJson()); - void updateChallengeData(UpdateChallengeDataDto dto) => _invokeWithRefresh("updateChallengeData", dto.toJson()); void requestEventData(RequestEventDataDto dto) => _invokeWithRefresh("requestEventData", dto.toJson()); + void requestFilteredEventIds(RequestFilteredEventsDto dto) => + _invokeWithRefresh("requestFilteredEventIds", dto.toJson()); + void requestRecommendedEvents(RequestRecommendedEventsDto dto) => _invokeWithRefresh("requestRecommendedEvents", dto.toJson()); diff --git a/game/lib/api/geopoint.dart b/game/lib/api/geopoint.dart index bb0b90ad..a17b6ae2 100644 --- a/game/lib/api/geopoint.dart +++ b/game/lib/api/geopoint.dart @@ -12,9 +12,6 @@ class GeoPoint { double get long => _long; double get heading => _heading; - static bool _isRequestingLocationPermissions = false; - static bool _isRequestingLocation = false; - GeoPoint( double lat, double long, @@ -25,38 +22,36 @@ class GeoPoint { _heading = heading; } - static Future current() async { + static Future current() async { var serviceEnabled = await Geolocator.isLocationServiceEnabled(); if (!serviceEnabled) { + print("Failed to enable location!!!!!!!!"); // Location services are not enabled don't continue // accessing the position and request users of the // App to enable the location services. return Future.error('Location services are disabled.'); } - if (_isRequestingLocationPermissions || _isRequestingLocation) { - //To handle the case where a request is already occuring. - - return null; - } - try { - _isRequestingLocationPermissions = true; var permission = await Geolocator.checkPermission(); if (permission == LocationPermission.denied) { + print("permissions denied"); permission = await Geolocator.requestPermission(); if (permission == LocationPermission.denied) { + print("permissions denied again"); return Future.error('Location services are disabled.'); } } if (permission == LocationPermission.deniedForever) { // Permissions are denied forever, handle appropriately. + print("permissions denied"); return Future.error( 'Location permissions are permanently denied, we cannot request permissions.'); } final pos = await Geolocator.getCurrentPosition(); return GeoPoint(pos.latitude, pos.longitude, pos.heading); } catch (e) { + print(e); return Future.error(e.toString()); } } diff --git a/game/lib/challenges/challenge_cell.dart b/game/lib/challenges/challenge_cell.dart index 0f5cae0e..6fbe82ca 100644 --- a/game/lib/challenges/challenge_cell.dart +++ b/game/lib/challenges/challenge_cell.dart @@ -1,10 +1,13 @@ import 'package:flutter/material.dart'; import 'package:game/preview/preview.dart'; +import 'package:flutter_svg/flutter_svg.dart'; class ChallengeCell extends StatefulWidget { final String location; final String challengeName; - final Image thumbnail; + final double? challengeLat; + final double? challengeLong; + final String imgUrl; final bool isCompleted; final String description; final String difficulty; @@ -14,7 +17,9 @@ class ChallengeCell extends StatefulWidget { const ChallengeCell( this.location, this.challengeName, - this.thumbnail, + this.challengeLat, + this.challengeLong, + this.imgUrl, this.isCompleted, this.description, this.difficulty, @@ -27,7 +32,9 @@ class ChallengeCell extends StatefulWidget { State createState() => _ChallengeCellState( location, challengeName, - thumbnail, + challengeLat, + challengeLong, + imgUrl, isCompleted, description, difficulty, @@ -38,7 +45,9 @@ class ChallengeCell extends StatefulWidget { class _ChallengeCellState extends State { final String location; final String challengeName; - final Image thumbnail; + final double? challengeLat; + final double? challengeLong; + final String imgUrl; final bool isCompleted; final String description; final String difficulty; @@ -50,7 +59,9 @@ class _ChallengeCellState extends State { _ChallengeCellState( this.location, this.challengeName, - this.thumbnail, + this.challengeLat, + this.challengeLong, + this.imgUrl, this.isCompleted, this.description, this.difficulty, @@ -70,8 +81,17 @@ class _ChallengeCellState extends State { ), context: context, isScrollControlled: true, - builder: (context) => Preview(challengeName, description, - difficulty, points, PreviewType.CHALLENGE, location, eventId)); + builder: (context) => Preview( + challengeName, + challengeLat, + challengeLong, + description, + imgUrl, + difficulty, + points, + PreviewType.CHALLENGE, + location, + eventId)); }, child: Container( decoration: BoxDecoration( @@ -89,13 +109,14 @@ class _ChallengeCellState extends State { child: Padding( padding: EdgeInsets.all(16.0), child: Row( - crossAxisAlignment: CrossAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, children: [ Padding( padding: const EdgeInsets.only(right: 14), child: ClipRRect( borderRadius: BorderRadius.all(Radius.circular(4.6)), - child: thumbnail, + child: Image.network(imgUrl, + width: 100, height: 100, fit: BoxFit.cover), ), ), Expanded( @@ -131,7 +152,7 @@ class _ChallengeCellState extends State { ), ), SizedBox( - height: 4, + height: 12, ), Row( mainAxisAlignment: MainAxisAlignment.start, @@ -147,33 +168,24 @@ class _ChallengeCellState extends State { difficulty, style: TextStyle( color: Color.fromARGB(204, 0, 0, 0), - fontSize: 14, + fontSize: 10, fontFamily: 'Poppins', - fontWeight: FontWeight.w600, + fontWeight: FontWeight.w300, ), ), ), SizedBox(width: 10), - Container( - padding: - EdgeInsets.symmetric(horizontal: 10, vertical: 2), - decoration: BoxDecoration( - border: Border.all( - color: Color.fromARGB(255, 255, 199, 55), - ), - color: Color.fromARGB(255, 189, 135, 31), - borderRadius: BorderRadius.circular(20), + Row(children: [ + SvgPicture.asset( + "assets/icons/bearcoins.svg", + width: 25, ), - child: Text( - points.toString() + "PTS", - style: TextStyle( - color: Colors.white, - fontSize: 14, - fontFamily: 'Poppins', - fontWeight: FontWeight.w600, - ), - ), - ), + Text(' ' + points.toString() + " PTS", + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: Color(0xFFC17E19))) + ]), ], ), ], diff --git a/game/lib/challenges/challenges_page.dart b/game/lib/challenges/challenges_page.dart index 7cfdb7ef..dc604397 100644 --- a/game/lib/challenges/challenges_page.dart +++ b/game/lib/challenges/challenges_page.dart @@ -1,21 +1,42 @@ import 'dart:io'; import 'package:flutter/material.dart'; -import 'package:flutter/src/foundation/key.dart'; -import 'package:flutter/src/widgets/framework.dart'; -import 'package:flutter/src/widgets/placeholder.dart'; import 'package:game/api/game_api.dart'; import 'package:game/api/game_client_dto.dart'; import 'package:game/model/challenge_model.dart'; import 'package:game/model/event_model.dart'; import 'package:game/model/group_model.dart'; import 'package:game/model/tracker_model.dart'; -import 'package:game/model/user_model.dart'; import 'package:game/utils/utility_functions.dart'; import 'package:provider/provider.dart'; import 'challenge_cell.dart'; import 'package:game/journeys/filter_form.dart'; +class ChallengeCellDto { + ChallengeCellDto({ + required this.location, + required this.name, + required this.lat, + required this.long, + required this.imgUrl, + required this.complete, + required this.description, + required this.difficulty, + required this.points, + required this.eventId, + }); + late String location; + late String name; + late double? lat; + late double? long; + late String imgUrl; + late bool complete; + late String description; + late String difficulty; + late int points; + late String eventId; +} + class ChallengesPage extends StatefulWidget { const ChallengesPage({Key? key}) : super(key: key); @@ -24,181 +45,239 @@ class ChallengesPage extends StatefulWidget { } class _ChallengesPageState extends State { - void openFilter() { - showModalBottomSheet( - context: context, - isScrollControlled: true, - builder: ( - BuildContext context, - ) { - return FilterForm(); - }); + List selectedCategories = []; + List selectedLocations = []; + String selectedDifficulty = ''; + + List eventData = []; + + // Callback function to receive updated state values from the child + void handleFilterSubmit(List? a, List? b, String c) { + setState(() { + selectedCategories = a ?? []; + selectedLocations = b ?? []; + selectedDifficulty = c; + }); } @override Widget build(BuildContext context) { return Scaffold( body: Container( - width: double.infinity, - height: double.infinity, decoration: BoxDecoration( color: Color.fromARGB(255, 255, 248, 241), ), child: Padding( padding: EdgeInsets.all(30), - child: Column( + child: Stack( children: [ - Container( - height: 30, - color: Color.fromARGB(51, 217, 217, 217), - child: TextField( - decoration: InputDecoration( - prefixIcon: Icon( - Icons.search, - color: Color.fromARGB(204, 0, 0, 0), - size: 12, - ), - border: OutlineInputBorder( - borderRadius: BorderRadius.all(Radius.circular(1.0))), - labelText: "Search a challenge name, location, etc...", - labelStyle: TextStyle( - color: Color.fromARGB(76, 0, 0, 0), - fontSize: 12, - fontFamily: 'Lato', - ), - ), + Align( + alignment: Alignment.bottomCenter, + child: Image( + image: AssetImage('assets/images/go-logo.png'), + width: MediaQuery.of(context).size.width / 3, + height: MediaQuery.of(context).size.height / 3, ), ), - Container( - padding: EdgeInsets.only(top: 10, bottom: 10), - child: Row( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Container( - height: 30, - child: TextButton.icon( - onPressed: openFilter, - icon: Icon( - Icons.filter_list_rounded, - color: Color.fromARGB(255, 0, 0, 0), - size: 20.0, - ), - style: ButtonStyle( - backgroundColor: MaterialStateProperty.all( - Color.fromARGB(153, 217, 217, 217)), - padding: MaterialStateProperty.all( - EdgeInsets.only(right: 16.0, left: 16.0), - ), - shape: MaterialStateProperty.all( - RoundedRectangleBorder( - borderRadius: BorderRadius.circular(3.0), - )), - ), - label: Text( - "Filter By", - style: TextStyle( - color: Color.fromARGB(255, 0, 0, 0), - fontSize: 15, - fontFamily: 'Inter', - ), - )), + Column( + children: [ + Container( + height: 30, + color: Color.fromARGB(51, 217, 217, 217), + child: TextField( + decoration: InputDecoration( + prefixIcon: Icon( + Icons.search, + color: Color.fromARGB(204, 0, 0, 0), + size: 12, + ), + border: OutlineInputBorder( + borderRadius: + BorderRadius.all(Radius.circular(1.0))), + labelText: + "Search a challenge name, location, etc...", + labelStyle: TextStyle( + color: Color.fromARGB(76, 0, 0, 0), + fontSize: 12, + fontFamily: 'Lato', + ), + ), ), - ], - ), - ), - Expanded(child: Consumer4( - builder: (context, myEventModel, groupModel, trackerModel, - challengeModel, child) { - List eventCells = []; - if (myEventModel.searchResults == null) { - myEventModel.searchEvents( - 0, - 1000, - [ - EventTimeLimitationDto.PERPETUAL, - EventTimeLimitationDto.LIMITED_TIME + ), + Container( + padding: EdgeInsets.only(top: 10, bottom: 10), + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Container( + height: 30, + child: TextButton.icon( + onPressed: () { + showModalBottomSheet( + context: context, + isScrollControlled: true, + builder: (context) => FilterForm( + onSubmit: handleFilterSubmit, + difficulty: selectedDifficulty, + locations: selectedLocations, + categories: selectedCategories), + ); + }, + icon: Icon( + Icons.filter_list_rounded, + color: Color.fromARGB(255, 0, 0, 0), + size: 20.0, + ), + style: ButtonStyle( + backgroundColor: + MaterialStateProperty.all( + Color.fromARGB(153, 217, 217, 217)), + padding: MaterialStateProperty.all( + EdgeInsets.only(right: 16.0, left: 16.0), + ), + shape: MaterialStateProperty.all( + RoundedRectangleBorder( + borderRadius: BorderRadius.circular(3.0), + )), + ), + label: Text( + "Filter By", + style: TextStyle( + color: Color.fromARGB(255, 0, 0, 0), + fontSize: 15, + fontFamily: 'Inter', + ), + )), + ), ], - false, - false, - false); - } - final events = myEventModel.searchResults ?? []; - if (!events - .any((element) => element.id == groupModel.curEventId)) { - final curEvent = - myEventModel.getEventById(groupModel.curEventId ?? ""); - if (curEvent != null) events.add(curEvent); - } - for (EventDto event in events) { - var tracker = trackerModel.trackerByEventId(event.id); - var numberCompleted = tracker?.prevChallenges?.length ?? 0; - var complete = - (numberCompleted == event.challenges?.length); - var locationCount = event.challenges?.length ?? 0; - DateTime now = DateTime.now(); - DateTime endtime = HttpDate.parse(event.endTime ?? ""); - - Duration timeTillExpire = endtime.difference(now); - if (locationCount != 1) continue; - var challenge = challengeModel - .getChallengeById(event.challenges?[0] ?? ""); - - if (challenge == null) continue; - if (!complete) - eventCells.add( - StreamBuilder( - stream: - Stream.fromFuture(Future.delayed(timeTillExpire)), - builder: (stream, value) => timeTillExpire.isNegative - ? Consumer( - builder: (context, apiClient, child) { - if (event.id == groupModel.curEventId) { - apiClient.serverApi?.setCurrentEvent( - SetCurrentEventDto(eventId: "")); - } - return Container(); - }, - ) - : ChallengeCell( - key: UniqueKey(), - friendlyLocation[challenge.location] ?? "", - event.name ?? "", - Image.network( - "https://picsum.photos/250?image=9"), - complete, - event.description ?? "", - friendlyDifficulty[event.difficulty] ?? "", - challenge.points ?? 0, - event.id), - ), - ); - } - return ListView.separated( - padding: const EdgeInsets.symmetric(horizontal: 3), - itemCount: eventCells.length + 1, - itemBuilder: (context, index) { - if (index == eventCells.length) { - // Footer widget - return Padding( - padding: const EdgeInsets.only(bottom: 50.0), - child: Center( - child: Image( - image: AssetImage('assets/images/go-logo.png'), - width: 200, - height: 200, - ), - )); + ), + ), + Expanded(child: Consumer5( + builder: (context, myEventModel, groupModel, + trackerModel, challengeModel, apiClient, child) { + if (myEventModel.searchResults == null) { + myEventModel.searchEvents( + 0, + 1000, + [ + EventTimeLimitationDto.PERPETUAL, + EventTimeLimitationDto.LIMITED_TIME + ], + false, + false, + false); + } + final events = myEventModel.searchResults ?? []; + if (!events.any( + (element) => element.id == groupModel.curEventId)) { + final curEvent = myEventModel + .getEventById(groupModel.curEventId ?? ""); + if (curEvent != null) events.add(curEvent); + } + eventData.clear(); + + for (EventDto event in events) { + var tracker = trackerModel.trackerByEventId(event.id); + var numberCompleted = + tracker?.prevChallenges.length ?? 0; + var complete = + (numberCompleted == event.challenges?.length); + var locationCount = event.challenges?.length ?? 0; + DateTime now = DateTime.now(); + DateTime endtime = HttpDate.parse(event.endTime ?? ""); + + Duration timeTillExpire = endtime.difference(now); + if (locationCount != 1) continue; + var challenge = challengeModel + .getChallengeById(event.challenges?[0] ?? ""); + + // print("Doing Event with now/endtime " + event.description.toString() + now.toString() + "/" + endtime.toString()); + if (challenge == null) { + // print("Challenge is null for event " + event.description.toString()); + + continue; + } + final challengeLocation = + challenge.location?.name ?? ""; + + bool eventMatchesDifficultySelection; + bool eventMatchesCategorySelection; + bool eventMatchesLocationSelection; + + if (selectedDifficulty.length == 0 || + selectedDifficulty == event.difficulty?.name) + eventMatchesDifficultySelection = true; + else + eventMatchesDifficultySelection = false; + + if (selectedLocations.length > 0) { + if (selectedLocations.contains(challengeLocation)) + eventMatchesLocationSelection = true; + else + eventMatchesLocationSelection = false; + } else + eventMatchesLocationSelection = true; + + if (selectedCategories.length > 0) { + if (selectedCategories.contains(event.category?.name)) + eventMatchesCategorySelection = true; + else + eventMatchesCategorySelection = false; + } else + eventMatchesCategorySelection = true; + if (!complete && + !timeTillExpire.isNegative && + eventMatchesDifficultySelection && + eventMatchesCategorySelection && + eventMatchesLocationSelection) { + eventData.add(ChallengeCellDto( + location: + friendlyLocation[challenge.location] ?? "", + name: event.name ?? "", + lat: challenge.latF ?? null, + long: challenge.longF ?? null, + imgUrl: challenge.imageUrl ?? + "https://upload.wikimedia.org/wikipedia/commons/b/b1/Missing-image-232x150.png", + complete: complete, + description: event.description ?? "", + difficulty: + friendlyDifficulty[event.difficulty] ?? "", + points: challenge.points ?? 0, + eventId: event.id, + )); + } else if (event.id == groupModel.curEventId) { + apiClient.serverApi?.setCurrentEvent( + SetCurrentEventDto(eventId: "")); + } } - return eventCells[index]; - }, - physics: BouncingScrollPhysics(), - separatorBuilder: (context, index) { - return SizedBox(height: 10); - }, - ); - })) + + return ListView.separated( + padding: const EdgeInsets.symmetric(horizontal: 3), + itemCount: eventData.length, + itemBuilder: (context, index) { + return ChallengeCell( + key: UniqueKey(), + eventData[index].location, + eventData[index].name, + eventData[index].lat, + eventData[index].long, + eventData[index].imgUrl, + eventData[index].complete, + eventData[index].description, + eventData[index].difficulty, + eventData[index].points, + eventData[index].eventId); + }, + physics: BouncingScrollPhysics(), + separatorBuilder: (context, index) { + return SizedBox(height: 10); + }, + ); + })) + ], + ), ], ), )), diff --git a/game/lib/details_page/details_page.dart b/game/lib/details_page/details_page.dart index 416d303b..5eb1fd9f 100644 --- a/game/lib/details_page/details_page.dart +++ b/game/lib/details_page/details_page.dart @@ -1,26 +1,19 @@ import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; -import 'package:game/journeys/journeys_page.dart'; -import 'package:game/main.dart'; -import 'package:game/navigation_page/bottom_navbar.dart'; +import 'package:game/api/game_client_dto.dart'; import 'package:game/interests/interests_page.dart'; import 'package:google_sign_in/google_sign_in.dart'; -import 'package:game/utils/utility_functions.dart'; -import 'package:game/gameplay/gameplay_page.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:game/details_page/dropdown_widget.dart'; -import 'package:velocity_x/velocity_x.dart'; - class DetailsPageWidget extends StatefulWidget { DetailsPageWidget( {Key? key, - required String this.userType, + required LoginEnrollmentTypeDto this.userType, required String? this.idToken, required GoogleSignInAccount? this.user}) : super(key: key); final scaffoldKey = GlobalKey(); - final String userType; + final LoginEnrollmentTypeDto userType; final String? idToken; final GoogleSignInAccount? user; @@ -30,7 +23,9 @@ class DetailsPageWidget extends StatefulWidget { class _DetailsPageWidgetState extends State { String _name = ""; - String? _college = "Arts and Sciences"; + String? _college; + String? _major; + String? _year; GoogleSignInAccount? user = null; @override void initState() { @@ -39,9 +34,6 @@ class _DetailsPageWidgetState extends State { final _formKey = GlobalKey(); - DropdownWidget collegeDropdown = - DropdownWidget(null, null, notifyParent: (val) {}); - List _colleges = [ "Agriculture and Life Sciences", "Architecture, Art and Planning", @@ -58,14 +50,8 @@ class _DetailsPageWidgetState extends State { // "Weill Cornell Medicine" ]; - DropdownWidget yearDropdown = - DropdownWidget(null, null, notifyParent: (val) {}); - List _years = ["2024", "2025", "2026", "2027"]; - DropdownWidget majorDropdown = - DropdownWidget(null, null, notifyParent: (val) {}); - Map> _majors = { "Agriculture and Life Sciences": [], "Architecture, Art and Planning": [], @@ -91,6 +77,17 @@ class _DetailsPageWidgetState extends State { @override Widget build(BuildContext context) { + // define major dropdown separately as it depends on the state of _college + DropdownWidget majorDropdown = DropdownWidget( + // assigning UniqueKey will rebuild widget upon state change + key: UniqueKey(), + null, + _college == null ? null : _majors[_college], + notifyParent: (val) { + _major = val; + }, + ); + return Scaffold( backgroundColor: Colors.white, body: Padding( @@ -100,7 +97,11 @@ class _DetailsPageWidgetState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - SvgPicture.asset("assets/icons/back.svg"), + TextButton( + onPressed: () { + Navigator.pop(context); + }, + child: SvgPicture.asset("assets/icons/back.svg")), SvgPicture.asset("assets/images/details_progress.svg"), SizedBox(height: 40.0), Column( @@ -169,7 +170,7 @@ class _DetailsPageWidgetState extends State { fontSize: 18, fontWeight: FontWeight.w700, )), - collegeDropdown = DropdownWidget( + DropdownWidget( null, _colleges, notifyParent: (val) => { @@ -189,11 +190,7 @@ class _DetailsPageWidgetState extends State { fontSize: 18, fontWeight: FontWeight.w700, )), - majorDropdown = DropdownWidget( - null, - _college == null ? null : _majors[_college], - notifyParent: (val) {}, - ) + majorDropdown ], ), SizedBox(height: 20), @@ -205,10 +202,12 @@ class _DetailsPageWidgetState extends State { fontSize: 18, fontWeight: FontWeight.w700, )), - yearDropdown = DropdownWidget( + DropdownWidget( null, _years, - notifyParent: (val) {}, + notifyParent: (val) { + _year = val; + }, ) ], ), @@ -223,9 +222,9 @@ class _DetailsPageWidgetState extends State { user: widget.user, idToken: widget.idToken, username: _name, - college: collegeDropdown.value, - major: majorDropdown.value, - year: yearDropdown.value, + college: _college, + major: _major, + year: _year, ), ), ); diff --git a/game/lib/details_page/dropdown_widget.dart b/game/lib/details_page/dropdown_widget.dart index 4bd3655a..3b2a6812 100644 --- a/game/lib/details_page/dropdown_widget.dart +++ b/game/lib/details_page/dropdown_widget.dart @@ -47,6 +47,7 @@ class _DropdownWidgetState extends State { )), buttonStyleData: ButtonStyleData( decoration: BoxDecoration( + color: Colors.white, border: Border.all( width: 2.0, color: Color.fromARGB(255, 217, 217, 217)), borderRadius: BorderRadius.circular(10.0)), @@ -58,7 +59,7 @@ class _DropdownWidgetState extends State { setState(() { value = newValue.toString(); }); - widget.notifyParent(value); + widget.notifyParent(newValue); }, hint: Text( "Select one", @@ -68,9 +69,6 @@ class _DropdownWidgetState extends State { fontFamily: 'Poppins', fontSize: 16), ), - // items: _college == null - // ? null - // : _majors[_college]!.mapIndexed((major, idx) { items: menuOptions == null ? null : menuOptions!.mapIndexed((item, idx) { diff --git a/game/lib/events_leaderboard/events_leaderboard_widget.dart b/game/lib/events_leaderboard/events_leaderboard_widget.dart deleted file mode 100644 index ce7b9f99..00000000 --- a/game/lib/events_leaderboard/events_leaderboard_widget.dart +++ /dev/null @@ -1,64 +0,0 @@ -import 'package:flutter/material.dart'; -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/widget/leaderboard_cell.dart'; -import 'package:game/widget/leaderboard_user_cell.dart'; -import 'package:provider/provider.dart'; - -class EventsLeaderboardWidget extends StatefulWidget { - EventsLeaderboardWidget({Key? key}) : super(key: key); - - @override - _EventsLeaderboardWidgetState createState() => - _EventsLeaderboardWidgetState(); -} - -class _EventsLeaderboardWidgetState extends State { - final scaffoldKey = GlobalKey(); - - @override - Widget build(BuildContext context) { - return Scaffold( - key: scaffoldKey, - floatingActionButton: backBtn(scaffoldKey, context, "Event 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: - (context, myGroupModel, myEventModel, myUserModel, child) { - int position = 1; - if (myGroupModel.curEventId == null) return ListView(); - final List list = myEventModel.getTopPlayersForEvent( - myGroupModel.curEventId!, 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 (LeaderDto user in list) - leaderBoardCell(context, user.username, position++, - user.score, user.userId == myUserModel.userData?.id) - ], - )) - ]); - }, - ), - ), - ), - ), - ); - } -} diff --git a/game/lib/gameplay/challenge_completed.dart b/game/lib/gameplay/challenge_completed.dart index e93d4e0d..20f52f2c 100644 --- a/game/lib/gameplay/challenge_completed.dart +++ b/game/lib/gameplay/challenge_completed.dart @@ -1,10 +1,4 @@ -import 'dart:math'; - import 'package:flutter/material.dart'; -import 'package:flutter/src/foundation/key.dart'; -import 'package:flutter/src/widgets/framework.dart'; -import 'package:game/api/geopoint.dart'; -import 'package:flutter/cupertino.dart'; import 'package:game/gameplay/gameplay_page.dart'; import 'package:game/navigation_page/bottom_navbar.dart'; import 'package:game/progress_indicators/circular_progress_indicator.dart'; @@ -43,27 +37,54 @@ class LoadingBar extends StatelessWidget { double progress = num_completed / num_challenges; return Row(mainAxisSize: MainAxisSize.max, children: [ Expanded( - flex: 8, - child: Container( - clipBehavior: Clip.hardEdge, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(15.0), - ), - height: 20.0, - width: double.infinity, - child: Stack( - alignment: Alignment.centerLeft, - children: [ - Positioned.fill( - child: LinearProgressIndicator( - value: progress, - color: Color(0xE6ED5656), - backgroundColor: Color(0xFFF1F1F1)), + flex: 8, + child: Stack(children: [ + Container( + width: MediaQuery.sizeOf(context).width * 0.66, + height: 24, + alignment: Alignment.centerLeft, + child: Container( + decoration: new BoxDecoration( + color: Color.fromARGB(255, 241, 241, 241), + shape: BoxShape.rectangle, + borderRadius: BorderRadius.all(Radius.circular(16.0)), + ), + ), + ), + Container( + width: + (progress + 0.05) * MediaQuery.sizeOf(context).width * 0.66, + height: 24, + alignment: Alignment.centerLeft, + child: Container( + decoration: new BoxDecoration( + color: Color(0xE6ED5656), + shape: BoxShape.rectangle, + borderRadius: BorderRadius.all(Radius.circular(16.0)), + ), ), - ], + ), + ]) + // child: Container( + // clipBehavior: Clip.hardEdge, + // decoration: BoxDecoration( + // borderRadius: BorderRadius.circular(15.0), + // ), + // height: 20.0, + // width: double.infinity, + // child: Stack( + // alignment: Alignment.centerLeft, + // children: [ + // Positioned.fill( + // child: LinearProgressIndicator( + // value: progress, + // color: Color(0xE6ED5656), + // backgroundColor: Color(0xFFF1F1F1)), + // ), + // ], + // ), + // ), ), - ), - ), Expanded( flex: 2, child: Row(children: [ @@ -124,9 +145,7 @@ class _ChallengeCompletedState extends State { // build list of completed challenge text fields to display later var total_pts = 0; List completedChallenges = []; - for (PrevChallengeDto prevChal in (tracker.prevChallenges ?? [])) { - print(prevChal.dateCompleted.toString()); - print(prevChal.hintsUsed); + for (PrevChallengeDto prevChal in tracker.prevChallenges) { var completedChal = challengeModel.getChallengeById(prevChal.challengeId); if (completedChal == null) continue; @@ -206,7 +225,7 @@ class _ChallengeCompletedState extends State { Container( padding: EdgeInsets.only(left: 30, bottom: 10), alignment: Alignment.centerLeft, - child: LoadingBar(tracker.prevChallenges?.length ?? 0, + child: LoadingBar(tracker.prevChallenges.length, event?.challenges?.length ?? 0)), Container( padding: EdgeInsets.only(left: 30, bottom: 10), @@ -226,7 +245,7 @@ class _ChallengeCompletedState extends State { child: Row( children: [ SvgPicture.asset( - 'assets/icons/locationCompleted.svg', // Replace with your SVG file path + 'assets/icons/locationCompleted.svg', fit: BoxFit.cover, ), Text( @@ -301,7 +320,7 @@ class _ChallengeCompletedState extends State { ), ], )), - if ((tracker.prevChallenges.last.hintsUsed ?? 0) > 2) + if ((tracker.prevChallenges.last.hintsUsed) > 2) Container( margin: EdgeInsets.only(left: 30, bottom: 10, right: 30), @@ -335,7 +354,7 @@ class _ChallengeCompletedState extends State { ? "Total Points: " + total_pts.toString() : "Points Earned: " + ((challenge.points ?? 0) - - (tracker.prevChallenges.last.hintsUsed ?? 0) * + (tracker.prevChallenges.last.hintsUsed) * hintsDeduction) .toString(), style: TextStyle( @@ -373,7 +392,7 @@ class _ChallengeCompletedState extends State { SvgPicture.asset("assets/icons/forwardcarrot.svg") ]), onPressed: () { - Navigator.push( + Navigator.pushReplacement( context, MaterialPageRoute( builder: (context) => BottomNavBar())); @@ -394,7 +413,7 @@ class _ChallengeCompletedState extends State { 10), // button's shape, ), ), - onPressed: () => Navigator.push( + onPressed: () => Navigator.pushReplacement( context, MaterialPageRoute( builder: (context) => BottomNavBar())), @@ -468,7 +487,7 @@ class _ChallengeCompletedState extends State { journeyPage = true; setState(() {}); } else { - Navigator.push( + Navigator.pushReplacement( context, MaterialPageRoute( builder: (context) => BottomNavBar())); diff --git a/game/lib/gameplay/gameplay_map.dart b/game/lib/gameplay/gameplay_map.dart index 072ab210..9fdbb305 100644 --- a/game/lib/gameplay/gameplay_map.dart +++ b/game/lib/gameplay/gameplay_map.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; +import 'package:game/navigation_page/bottom_navbar.dart'; +import 'package:game/splash_page/splash_page.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart'; import 'package:game/api/geopoint.dart'; import 'package:geolocator/geolocator.dart'; @@ -7,21 +8,16 @@ import 'package:flutter_svg/flutter_svg.dart'; import 'dart:async'; import 'dart:math'; import 'package:game/gameplay/challenge_completed.dart'; -import 'package:game/progress_indicators/circular_progress_indicator.dart'; import 'package:game/utils/utility_functions.dart'; // for backend connection import 'package:provider/provider.dart'; import 'package:game/api/game_client_dto.dart'; import 'package:game/api/game_api.dart'; -import 'package:game/model/event_model.dart'; import 'package:game/model/tracker_model.dart'; import 'package:game/model/group_model.dart'; import 'package:game/model/challenge_model.dart'; -import 'package:provider/provider.dart'; -import 'package:velocity_x/velocity_x.dart'; - class GameplayMap extends StatefulWidget { final GeoPoint targetLocation; final double awardingRadius; @@ -275,239 +271,250 @@ class _GameplayMapState extends State { // builder: (context, AsyncSnapshot snapshot) { // return !snapshot.hasData // ? CircularIndicator() + final client = Provider.of(context); + return MaterialApp( theme: ThemeData( useMaterial3: true, colorSchemeSeed: Colors.green[700], ), - home: Consumer2( - builder: (context, groupModel, trackerModel, child) { + home: Consumer3( + builder: (context, groupModel, trackerModel, challengeModel, child) { EventTrackerDto? tracker = trackerModel.trackerByEventId(groupModel.curEventId ?? ""); if (tracker == null) { displayToast("Error getting event tracker", Status.error); } else { - numHintsLeft = totalHints - (tracker.hintsUsed ?? 0); + numHintsLeft = totalHints - (tracker.hintsUsed); } + var challenge = + challengeModel.getChallengeById(tracker!.curChallengeId); return Scaffold( body: Stack( - alignment: Alignment.bottomCenter, - children: [ - Listener( - onPointerDown: (e) { - cancelRecenterCamera(); - }, - child: GoogleMap( - onMapCreated: _onMapCreated, - compassEnabled: false, - myLocationButtonEnabled: false, - zoomControlsEnabled: false, - myLocationEnabled: false, - mapToolbarEnabled: false, - mapType: MapType.normal, - initialCameraPosition: CameraPosition( - target: currentLocation == null - ? _center - : LatLng(currentLocation!.lat, currentLocation!.lat), - zoom: 11, - ), - markers: { - Marker( - markerId: const MarkerId("currentLocation"), - icon: currentLocationIcon, - position: currentLocation == null - ? _center - : LatLng( - currentLocation!.lat, currentLocation!.long), - rotation: currentLocation == null - ? 0 - : currentLocation!.heading, - ), - }, - circles: { - Circle( - circleId: CircleId("hintCircle"), - center: hintCenter != null - ? LatLng(hintCenter!.lat, hintCenter!.long) - : _center, - radius: hintRadius, - strokeColor: Color.fromARGB(80, 30, 41, 143), - strokeWidth: 2, - fillColor: Color.fromARGB(80, 83, 134, 237), - ) - }, + alignment: Alignment.bottomCenter, + children: [ + StreamBuilder( + stream: client.clientApi.disconnectedStream, + builder: ((context, snapshot) { + if (client.serverApi == null) { + WidgetsBinding.instance.addPostFrameCallback((_) { + Navigator.pushReplacement( + context, + MaterialPageRoute( + builder: (context) => SplashPageWidget())); + displayToast("Lost connection!", Status.success); + }); + } + + return Container(); + })), + Listener( + onPointerDown: (e) { + cancelRecenterCamera(); + }, + child: GoogleMap( + onMapCreated: _onMapCreated, + compassEnabled: false, + myLocationButtonEnabled: false, + zoomControlsEnabled: false, + myLocationEnabled: false, + mapToolbarEnabled: false, + mapType: MapType.normal, + initialCameraPosition: CameraPosition( + target: currentLocation == null + ? _center + : LatLng(currentLocation!.lat, currentLocation!.lat), + zoom: 11, + ), + markers: { + Marker( + markerId: const MarkerId("currentLocation"), + icon: currentLocationIcon, + position: currentLocation == null + ? _center + : LatLng(currentLocation!.lat, currentLocation!.long), + rotation: + currentLocation == null ? 0 : currentLocation!.heading, + ), + }, + circles: { + Circle( + circleId: CircleId("hintCircle"), + center: hintCenter != null + ? LatLng(hintCenter!.lat, hintCenter!.long) + : _center, + radius: hintRadius, + strokeColor: Color.fromARGB(80, 30, 41, 143), + strokeWidth: 2, + fillColor: Color.fromARGB(80, 83, 134, 237), + ) + }, + ), + ), + Container( + margin: EdgeInsets.only(bottom: 70), + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: Color.fromARGB(255, 237, 86, 86), + padding: + EdgeInsets.only(right: 15, left: 15, top: 10, bottom: 10), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), // button's shape ), ), - Container( - margin: EdgeInsets.only(bottom: 70), - child: ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: Color.fromARGB(255, 237, 86, 86), - padding: EdgeInsets.only( - right: 15, left: 15, top: 10, bottom: 10), - shape: RoundedRectangleBorder( - borderRadius: - BorderRadius.circular(10), // button's shape - ), - ), - child: Text( - "I've Arrived!", - style: TextStyle( - fontFamily: 'Poppins', - fontSize: 21, - fontWeight: FontWeight.w400, - color: Color(0xFFFFFFFF)), - ), - onPressed: () { - showDialog( - context: context, - builder: (context) { - return Container( - color: Colors.white - .withOpacity(0.3), // Adjust opacity as needed - width: MediaQuery.of(context).size.width, - height: MediaQuery.of(context).size.height, - child: Container( - margin: EdgeInsetsDirectional.only( - start: 10, end: 10), - child: Dialog( - elevation: 16, //arbitrary large number - child: ClipRRect( - borderRadius: BorderRadius.circular( - 10), // Same as the Dialog's shape - child: displayDialogue(), - ), - ), - ), - ); - }, + child: Text( + "I've Arrived!", + style: TextStyle( + fontFamily: 'Poppins', + fontSize: 21, + fontWeight: FontWeight.w400, + color: Color(0xFFFFFFFF)), + ), + onPressed: () { + showDialog( + context: context, + builder: (context) { + return Container( + margin: EdgeInsetsDirectional.only(start: 10, end: 10), + child: Dialog( + elevation: 16, //arbitrary large number + child: ClipRRect( + borderRadius: BorderRadius.circular( + 10), // Same as the Dialog's shape + child: displayDialogue(), + ), + ), ); }, - ), - ), - ], + ); + }, + ), ), - floatingActionButton: Stack( - alignment: AlignmentDirectional.topEnd, - children: [ - Column( - mainAxisAlignment: MainAxisAlignment.end, - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Padding( - padding: EdgeInsets.only(bottom: 15.0, right: 10.0), - child: Stack( - children: [ - FloatingActionButton.extended( - onPressed: useHint, - label: SvgPicture.asset("assets/icons/maphint.svg", - colorFilter: ColorFilter.mode( - Color.fromARGB(255, 131, 90, 124), - BlendMode.srcIn)), - backgroundColor: Color.fromARGB(255, 255, 255, 255), - shape: CircleBorder(), - ), - Positioned( - top: -5, - right: 0, - child: Container( - padding: EdgeInsets.all(5.0), - decoration: BoxDecoration( - shape: BoxShape.circle, - color: Colors.white, - boxShadow: [ - BoxShadow( - color: Colors.black - .withOpacity(0.3), // Shadow color - blurRadius: 5, // Spread radius - offset: Offset(2, - 2), // Shadow position, you can adjust this - ), - ], - ), - child: Text( - numHintsLeft.toString(), - style: TextStyle( - color: Color.fromARGB(255, 131, 90, 124), - fontSize: 15, - fontWeight: FontWeight.bold, + Positioned( + bottom: 0, + right: 10, + child: Column( + children: [ + Container( + padding: EdgeInsets.only(bottom: 15.0), + child: Stack( + children: [ + // hint button + FloatingActionButton.extended( + onPressed: useHint, + label: SvgPicture.asset("assets/icons/maphint.svg", + colorFilter: ColorFilter.mode( + numHintsLeft == 0 + ? Color.fromARGB(255, 217, 217, 217) + : Color.fromARGB(255, 131, 90, 124), + BlendMode.srcIn)), + backgroundColor: Color.fromARGB(255, 255, 255, 255), + shape: CircleBorder(), + ), + // num hints left counter + Positioned( + top: -5, + right: 0, + child: Container( + padding: EdgeInsets.all(5.0), + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Colors.white, + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.3), + blurRadius: 5, ), + ], + ), + child: Text( + numHintsLeft.toString(), + style: TextStyle( + color: numHintsLeft == 0 + ? Color.fromARGB(255, 217, 217, 217) + : Color.fromARGB(255, 131, 90, 124), + fontSize: 15, + fontWeight: FontWeight.bold, ), ), ), - ], - ), + ), + ], ), - Padding( - padding: EdgeInsets.only(bottom: 150.0), - child: FloatingActionButton.extended( - onPressed: recenterCamera, - label: SvgPicture.asset("assets/icons/maprecenter.svg", - colorFilter: ColorFilter.mode( - Color.fromARGB(255, 131, 90, 124), - BlendMode.srcIn)), - backgroundColor: Color.fromARGB(255, 255, 255, 255), - shape: CircleBorder(), - ), + ), + Padding( + padding: EdgeInsets.only(bottom: 150.0), + child: FloatingActionButton.extended( + onPressed: recenterCamera, + label: SvgPicture.asset("assets/icons/maprecenter.svg", + colorFilter: ColorFilter.mode( + Color.fromARGB(255, 131, 90, 124), + BlendMode.srcIn)), + backgroundColor: Color.fromARGB(255, 255, 255, 255), + shape: CircleBorder(), ), - ], - ), - Padding( - // expandable image in top right of map - padding: EdgeInsets.only(left: 10.0, right: 10, top: 40.0), - child: GestureDetector( - onTap: () { - isExpanded - ? setState(() { - isExpanded = false; - pictureHeight = 80.0; - pictureWidth = 80.0; - pictureIcon = SvgPicture.asset( - "assets/icons/mapexpand.svg"); - pictureAlign = Alignment.topRight; - }) - : setState(() { - isExpanded = true; - pictureHeight = - MediaQuery.of(context).size.height * 0.6; - pictureWidth = - MediaQuery.of(context).size.width * 0.85; - pictureIcon = - SvgPicture.asset("assets/icons/mapexit.svg"); - pictureAlign = Alignment.topCenter; - }); - }, - child: AnimatedContainer( - duration: Duration(milliseconds: 50), - width: pictureWidth, - height: pictureHeight, - child: Stack( - children: [ - Container( - alignment: pictureAlign, - child: ClipRRect( - borderRadius: BorderRadius.circular(10), - child: Image.asset( - 'assets/images/main-bg.jpeg', - fit: BoxFit.cover, - width: pictureWidth, - height: pictureHeight, - ), - ), - ), - Padding( - padding: EdgeInsets.all(4.0), - child: Container( - alignment: Alignment.topRight, - child: pictureIcon), + ), + ], + ), + ), + Positioned( + // expandable image in top right of map + // padding: EdgeInsets.only(left: 10.0, right: 10, top: 0.0), + top: MediaQuery.of(context).size.width * 0.05, + right: MediaQuery.of(context).size.width * 0.05, + child: GestureDetector( + onTap: () { + isExpanded + ? setState(() { + isExpanded = false; + pictureHeight = 80.0; + pictureWidth = 80.0; + pictureIcon = + SvgPicture.asset("assets/icons/mapexpand.svg"); + pictureAlign = Alignment.topRight; + }) + : setState(() { + isExpanded = true; + pictureHeight = + MediaQuery.of(context).size.height * 0.6; + pictureWidth = + MediaQuery.of(context).size.width * 0.90; + pictureIcon = + SvgPicture.asset("assets/icons/mapexit.svg"); + pictureAlign = Alignment.topCenter; + }); + }, + child: AnimatedContainer( + duration: Duration(milliseconds: 100), + width: pictureWidth, + height: pictureHeight, + child: Stack( + children: [ + Container( + alignment: pictureAlign, + child: ClipRRect( + borderRadius: BorderRadius.circular(10), + child: Image.network( + challenge?.imageUrl ?? + "https://upload.wikimedia.org/wikipedia/commons/b/b1/Missing-image-232x150.png", + fit: BoxFit.cover, + width: pictureWidth, + height: pictureHeight, ), - ], + ), ), - ), + Padding( + padding: EdgeInsets.all(4.0), + child: Container( + alignment: Alignment.topRight, child: pictureIcon), + ), + ], ), ), - ], - )); + ), + ), + ], + )); }), ); // }); @@ -581,9 +588,6 @@ class _GameplayMapState extends State { var eventId = Provider.of(context, listen: false) .curEventId; - var event = - Provider.of(context, listen: false) - .getEventById(eventId ?? ""); var tracker = Provider.of(context, listen: false) .trackerByEventId(eventId ?? ""); @@ -595,7 +599,7 @@ class _GameplayMapState extends State { } else { var challenge = Provider.of(context, listen: false) - .getChallengeById(tracker.curChallengeId!); + .getChallengeById(tracker.curChallengeId); if (challenge == null) { displayToast( "An error occurred while getting challenge", diff --git a/game/lib/gameplay/gameplay_page.dart b/game/lib/gameplay/gameplay_page.dart index 827130a8..6e7bb656 100644 --- a/game/lib/gameplay/gameplay_page.dart +++ b/game/lib/gameplay/gameplay_page.dart @@ -1,24 +1,16 @@ -import 'dart:math'; - -import 'package:flutter/material.dart'; -import 'package:flutter/src/foundation/key.dart'; -import 'package:flutter/src/widgets/framework.dart'; -import 'package:flutter/src/widgets/placeholder.dart'; import 'package:flutter/material.dart'; import 'package:game/api/game_api.dart'; import 'package:game/model/event_model.dart'; import 'package:game/model/tracker_model.dart'; import 'package:game/model/group_model.dart'; import 'package:game/api/geopoint.dart'; -import 'package:game/navigation_page/home_navbar.dart'; -import 'package:game/utils/utility_functions.dart'; +import 'package:game/navigation_page/bottom_navbar.dart'; import 'package:geolocator/geolocator.dart'; import 'package:game/model/challenge_model.dart'; import 'gameplay_map.dart'; import 'package:provider/provider.dart'; -import 'package:game/api/game_client_dto.dart'; + import 'package:game/progress_indicators/circular_progress_indicator.dart'; -import 'package:flutter/cupertino.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'dart:async'; @@ -42,6 +34,17 @@ class _GameplayPageState extends State { late StreamSubscription positionStream; + final Map friendlyLocation = { + "ENG_QUAD": "Eng Quad", + "ARTS_QUAD": "Arts Quad", + "AG_QUAD": "Ag Quad", + "NORTH_CAMPUS": "North Campus", + "WEST_CAMPUS": "West Campus", + "COLLEGETOWN": "Collegetown", + "ITHACA_COMMONS": "Ithaca Commons", + "ANY": "Cornell", + }; + @override void initState() { startPositionStream(); @@ -95,7 +98,7 @@ class _GameplayPageState extends State { return CircularIndicator(); } - var challenge = challengeModel.getChallengeById(tracker.curChallengeId!); + var challenge = challengeModel.getChallengeById(tracker.curChallengeId); if (challenge == null) { return Scaffold( @@ -131,7 +134,12 @@ class _GameplayPageState extends State { foregroundColor: Colors.grey), onPressed: () { // Left button action - Navigator.pop(context); + Navigator.pushReplacement( + context, + MaterialPageRoute( + builder: (context) => + BottomNavBar()), + ); }, child: Row(children: [ SvgPicture.asset( @@ -156,13 +164,7 @@ class _GameplayPageState extends State { vertical: 4.0, horizontal: 8.0), child: Text( (event.challenges!.length > 1 - ? "Journey " + - (tracker.prevChallenges.length + - 1) - .toString() + - "/" + - event.challenges!.length - .toString() + ? "Journey" : "Challenge"), style: TextStyle( fontSize: 14, @@ -173,7 +175,7 @@ class _GameplayPageState extends State { margin: EdgeInsets.only(top: 16.45, bottom: 11), alignment: Alignment.centerLeft, child: Text( - "Find the Location of ${challenge.description}!", + challenge.description ?? "NO DESCRIPTION", textAlign: TextAlign.left, style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold), @@ -191,7 +193,7 @@ class _GameplayPageState extends State { Text( ' ' + (friendlyLocation[ - challenge.location?.name] ?? + challenge.location] ?? ""), style: TextStyle( fontSize: 12, @@ -217,13 +219,6 @@ class _GameplayPageState extends State { "assets/icons/bearcoins.svg"), Text( ' ' + - ((tracker.hintsUsed > 0) - ? ((challenge.points ?? 0) - - tracker.hintsUsed * - 25) - .toString() + - '/' - : '') + (challenge.points ?? 0).toString() + " PTS", style: TextStyle( diff --git a/game/lib/global_leaderboard/global_leaderboard_widget.dart b/game/lib/global_leaderboard/global_leaderboard_widget.dart index 78ad5e32..027e9998 100644 --- a/game/lib/global_leaderboard/global_leaderboard_widget.dart +++ b/game/lib/global_leaderboard/global_leaderboard_widget.dart @@ -1,4 +1,7 @@ +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_svg/svg.dart'; import 'package:game/api/game_client_dto.dart'; import 'package:game/model/event_model.dart'; import 'package:game/model/group_model.dart'; @@ -6,6 +9,7 @@ import 'package:game/model/user_model.dart'; import 'package:game/global_leaderboard/podium_widgets.dart'; import 'package:game/widget/leaderboard_cell.dart'; import 'package:game/widget/podium_cell.dart'; +import 'package:geolocator/geolocator.dart'; import 'package:provider/provider.dart'; /** @@ -37,178 +41,156 @@ class _GlobalLeaderboardWidgetState extends State { letterSpacing: 0.0, ); + int playerPosition = 0; + var scoreList = []; + return Scaffold( - key: scaffoldKey, - backgroundColor: Color.fromARGB(255, 255, 248, 241), - appBar: AppBar( - toolbarHeight: 85, - automaticallyImplyLeading: false, - backgroundColor: Color.fromARGB(255, 237, 86, 86), - flexibleSpace: FlexibleSpaceBar( - title: Text( + key: scaffoldKey, + backgroundColor: Color.fromARGB(255, 255, 248, 241), + appBar: AppBar( + toolbarHeight: 85, + automaticallyImplyLeading: false, + backgroundColor: Color.fromARGB(255, 237, 86, 86), + flexibleSpace: FlexibleSpaceBar( + title: Padding( + padding: const EdgeInsets.only(bottom: 8), + child: Text( 'Leaderboard', style: leaderboardStyle, ), - centerTitle: true), - actions: [], - ), - body: Padding( - padding: const EdgeInsets.only(top: 0), - child: Column( - children: [ - //Podium Container - Consumer3(builder: - (context, myGroupModel, myEventModel, myUserModel, child) { - //Loading in the lists and then creating podiumList of top 3 - final List list = - myEventModel.getTopPlayersForEvent('', 1000); - - list.sort((a, b) => b.score.compareTo(a.score)); - LeaderDto empty = LeaderDto( - userId: " ", - username: " ", - score: 0, - ); + ), + centerTitle: true), + actions: [], + ), + body: Consumer3( + builder: (context, myGroupModel, myEventModel, myUserModel, child) { + //Loading in the lists and then creating podiumList of top 3 + final List? list = + myEventModel.getTopPlayersForEvent('', 1000); - // Creating list to be displayed within the podium (filled with empty users if lists length is less than 3) - List fullList = List.from(list); + if (list == null) + return Center( + child: CircularProgressIndicator(), + ); - int iterTillFull = 3 - fullList.length; - if (fullList.length < 3) { - for (int i = 0; i < iterTillFull; i++) { - fullList.add(empty); - } - } + LeaderDto empty = LeaderDto( + userId: " ", + username: " ", + score: 0, + ); - List podiumList = fullList.sublist(0, 3); + // Creating list to be displayed within the podium (filled with empty users if lists length is less than 3) + List fullList = List.from(list); - // Booleans representing whether the current player is in the podium for highlighting purposes - bool firstPodiumUser = podiumList.length > 0 && - podiumList[0].userId == myUserModel.userData?.id; + int iterTillFull = 3 - fullList.length; + if (fullList.length < 3) { + for (int i = 0; i < iterTillFull; i++) { + fullList.add(empty); + } + } + // Leaderboard starts at 4th position because first three already in podium + int position = 4; - bool secondPodiumUser = podiumList.length > 1 && - podiumList[1].userId == myUserModel.userData?.id; + List podiumList = fullList.sublist(0, 3); - bool thirdPodiumUser = podiumList.length > 2 && - podiumList[2].userId == myUserModel.userData?.id; + bool firstPodiumUser = podiumList.length > 0 && + podiumList[0].userId == myUserModel.userData?.id; - 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) - : podiumCell(context, ""), - SizedBox(height: 12), - SecondPodium( - context, podiumList[1].score, secondPodiumUser), - ], - ), - SizedBox(width: 5), - Column( - children: [ - podiumList.length > 0 - ? podiumCell(context, podiumList[0].username) - : podiumCell(context, ""), - SizedBox(height: 12), - FirstPodium( - context, podiumList[0].score, firstPodiumUser), - ], - ), - SizedBox(width: 5), - Column( - children: [ - SizedBox(height: 50), - podiumList.length > 2 - ? podiumCell(context, podiumList[2].username) - : podiumCell(context, ""), - SizedBox(height: 12), - ThirdPodium( - context, podiumList[2].score, thirdPodiumUser), - ], - ), - ]), - ); - }), - SizedBox(height: 5), - //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) { - // Use this line below to retrieve actual data - final List list = - myEventModel.getTopPlayersForEvent('', 1000); - // Leaderboard starts at 4th position because first three already in podium - int position = 4; + bool secondPodiumUser = podiumList.length > 1 && + podiumList[1].userId == myUserModel.userData?.id; - list.sort((a, b) => b.score.compareTo(a.score)); + bool thirdPodiumUser = podiumList.length > 2 && + podiumList[2].userId == myUserModel.userData?.id; - return Container( - width: 345.0, - height: 446.0, - decoration: BoxDecoration( - color: Color.fromRGBO(255, 170, 91, 0.15), - borderRadius: BorderRadius.only( - topLeft: Radius.circular(10.0), - topRight: Radius.circular(10.0), - ), - ), + return Center( + //Podium Container + child: Column(children: [ + Padding( + padding: + EdgeInsets.only(top: MediaQuery.sizeOf(context).width * 0.05), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Column(mainAxisAlignment: MainAxisAlignment.end, children: [ + podiumList.length > 1 + ? podiumCell( + context, podiumList[1].username, secondPodiumUser) + : podiumCell(context, "", false), + SecondPodium(context, podiumList[1].score, secondPodiumUser) + ]), + SizedBox(width: MediaQuery.sizeOf(context).width * 0.03), + Column(mainAxisAlignment: MainAxisAlignment.end, children: [ + podiumList.length > 0 + ? podiumCell( + context, podiumList[0].username, firstPodiumUser) + : podiumCell(context, "", false), + FirstPodium(context, podiumList[0].score, firstPodiumUser) + ]), + SizedBox(width: MediaQuery.sizeOf(context).width * 0.03), + Column(mainAxisAlignment: MainAxisAlignment.end, children: [ + podiumList.length > 2 + ? podiumCell( + context, podiumList[2].username, thirdPodiumUser) + : podiumCell(context, "", false), + ThirdPodium(context, podiumList[2].score, thirdPodiumUser) + ]), + ]), + ), + SizedBox(height: MediaQuery.sizeOf(context).height * 0.01), + Expanded( + child: Container( + width: 360.0, + height: 446.0, + decoration: BoxDecoration( + color: Color.fromRGBO(255, 170, 91, 0.15), + borderRadius: BorderRadius.only( + topLeft: Radius.circular(10.0), + topRight: Radius.circular(10.0), + ), + ), + child: ListView( + shrinkWrap: true, + scrollDirection: Axis.vertical, + children: [ + for (LeaderDto user in list.skip(3)) + Align( + alignment: Alignment.center, + child: Padding( + padding: const EdgeInsets.only(top: 16), child: Container( - width: 283.05, - height: 432.0, - child: ListView( - shrinkWrap: true, - scrollDirection: Axis.vertical, - children: [ - for (LeaderDto user in list.skip(3)) - Padding( - padding: const EdgeInsets.only( - left: 30.95, right: 30.95, top: 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 == myUserModel.userData?.id, - ), - ), - ), + 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.745), + blurRadius: 6.989011764526367, + ), ], ), + child: leaderBoardCell( + context, + user.username, + position++, + user.score, + user.userId == myUserModel.userData?.id, + ), ), - ); - }, - ), - ), - ) - ], - ), - )); + ), + ), + ], + ), + ), + ) + ])); + }), + ); } } diff --git a/game/lib/global_leaderboard/podium_widgets.dart b/game/lib/global_leaderboard/podium_widgets.dart index 40c89277..921ff5ed 100644 --- a/game/lib/global_leaderboard/podium_widgets.dart +++ b/game/lib/global_leaderboard/podium_widgets.dart @@ -1,10 +1,18 @@ +import 'dart:ui'; + 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 (FirstPodium, SecndPodium, ThridPodium) there is a red and orange asset svg variation, - * which is orange if that is the user's current spot and red otherwise. + * 'PodiumBlock' widget - Displays leaderboard podium + * + * This file contains a dynamic podium widget block to appear in the leaderboard + * page. For each of the 4 possible podium positions (1st,2nd,3rd,Not in top 3) there is, + * there is a differet image variation which highlights the corresponding podium in orange. + * + * @param props - Contains: + * - 'position': Current position of user + * - 'scoreList': Number of points for the top 3 users */ var pointsStyle = TextStyle( @@ -21,28 +29,44 @@ Widget FirstPodium(context, int points, bool isUser) { ? 'assets/icons/podium1highlighted.svg' : 'assets/icons/podium1red.svg'; - return Center( - child: Container( - width: 106, - height: 112, - child: - Stack(alignment: FractionalOffset(0.5, 0.58), children: [ - SvgPicture.asset( - svgAssetPath, - semanticsLabel: '1st Podium', + return Align( + alignment: Alignment.bottomCenter, + child: Stack(alignment: Alignment.bottomCenter, children: [ + SvgPicture.asset( + width: MediaQuery.sizeOf(context).width * 0.25, + isUser + ? 'assets/icons/podium1user.svg' + : 'assets/icons/podium1blank.svg', + semanticsLabel: '1st Podium', + ), + Positioned( + bottom: MediaQuery.sizeOf(context).height * 0.025, + child: Container( + width: 68, + height: 29, + padding: const EdgeInsets.symmetric(horizontal: 2, vertical: 2), + decoration: ShapeDecoration( + color: Color.fromRGBO(234, 177, 17, 1), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), ), - - SizedBox( - width: 60, - height: 29, - child: FittedBox( - alignment: Alignment.center, - fit: BoxFit.scaleDown, - child: Text(points.toString() + " PTS", style: pointsStyle), + ), + child: Center( + child: Text( + points.toString() + " PTS", + style: TextStyle( + color: Colors.white, + fontSize: 14, + fontFamily: 'Poppins', + fontWeight: FontWeight.w400, + height: 0, ), ), - // Text(points.toString() + " PTS" - ]))); + ), + ), + ), + ]), + ); } Widget SecondPodium(context, int points, bool isUser) { @@ -50,54 +74,87 @@ Widget SecondPodium(context, int points, bool isUser) { ? 'assets/icons/podium2highlighted.svg' : 'assets/icons/podium2red.svg'; - return Center( - child: Container( - width: 106, - height: 86, - child: - Stack(alignment: FractionalOffset(0.5, 0.63), children: [ - SvgPicture.asset( - svgAssetPath, - semanticsLabel: '2nd Podium', + return Align( + alignment: Alignment.bottomCenter, + child: Stack(alignment: Alignment.bottomCenter, children: [ + SvgPicture.asset( + width: MediaQuery.sizeOf(context).width * 0.25, + isUser + ? 'assets/icons/podium2user.svg' + : 'assets/icons/podium2blank.svg', + semanticsLabel: '2nd Podium', + ), + Positioned( + bottom: MediaQuery.sizeOf(context).height * 0.01, + child: Container( + width: 68, + height: 29, + padding: const EdgeInsets.symmetric(horizontal: 2, vertical: 2), + decoration: ShapeDecoration( + color: Color.fromRGBO(171, 173, 173, 1), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), ), - - SizedBox( - width: 60, - height: 29, - child: FittedBox( - alignment: Alignment.center, - fit: BoxFit.scaleDown, - child: Text(points.toString() + " PTS", style: pointsStyle), + ), + child: Center( + child: Text( + points.toString() + " PTS", + style: TextStyle( + color: Colors.white, + fontSize: 14, + fontFamily: 'Poppins', + fontWeight: FontWeight.w400, + height: 0, ), ), - // Text(points.toString() + " PTS" - ]))); + ), + ), + ), + ]), + ); } Widget ThirdPodium(context, int points, bool isUser) { String svgAssetPath = isUser ? 'assets/icons/podium3highlighted.svg' : 'assets/icons/podium3red.svg'; - return Center( - child: Container( - width: 106, - height: 62, - child: - Stack(alignment: FractionalOffset(0.5, 0.75), children: [ - SvgPicture.asset( - svgAssetPath, - semanticsLabel: '3rd Podium', - ), - SizedBox( - width: 60, - height: 29, - child: FittedBox( - alignment: Alignment.center, - fit: BoxFit.scaleDown, - child: Text(points.toString() + " PTS", style: pointsStyle), + return Align( + alignment: Alignment.bottomCenter, + child: Stack(alignment: Alignment.bottomCenter, children: [ + SvgPicture.asset( + width: MediaQuery.sizeOf(context).width * 0.25, + isUser + ? 'assets/icons/podium3user.svg' + : 'assets/icons/podium3blank.svg', + semanticsLabel: '3rd Podium', + ), + Positioned( + bottom: MediaQuery.sizeOf(context).height * 0.005, + child: Container( + width: 68, + height: 29, + padding: const EdgeInsets.symmetric(horizontal: 2, vertical: 2), + decoration: ShapeDecoration( + color: Color.fromRGBO(219, 120, 42, 1), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + ), + child: Center( + child: Text( + points.toString() + " PTS", + style: TextStyle( + color: Colors.white, + fontSize: 14, + fontFamily: 'Poppins', + fontWeight: FontWeight.w400, + height: 0, ), ), - // Text(points.toString() + " PTS" - ]))); + ), + ), + ), + ]), + ); } diff --git a/game/lib/interests/interests_page.dart b/game/lib/interests/interests_page.dart index ed03eb88..f555c205 100644 --- a/game/lib/interests/interests_page.dart +++ b/game/lib/interests/interests_page.dart @@ -1,17 +1,14 @@ import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; -import 'package:game/journeys/journeys_page.dart'; +import 'package:game/api/game_client_dto.dart'; import 'package:game/main.dart'; -import 'package:game/navigation_page/bottom_navbar.dart'; import 'package:google_sign_in/google_sign_in.dart'; import 'package:game/utils/utility_functions.dart'; import 'package:flutter_svg/flutter_svg.dart'; -import 'package:dropdown_button2/dropdown_button2.dart'; class InterestsPageWidget extends StatefulWidget { InterestsPageWidget( {Key? key, - required String this.userType, + required LoginEnrollmentTypeDto this.userType, required String? this.idToken, required GoogleSignInAccount? this.user, required String this.username, @@ -20,7 +17,7 @@ class InterestsPageWidget extends StatefulWidget { required String? this.year}) : super(key: key); final scaffoldKey = GlobalKey(); - final String userType; + final LoginEnrollmentTypeDto userType; final String? idToken; final GoogleSignInAccount? user; final String username; @@ -55,7 +52,11 @@ class _InterestsPageWidgetState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - SvgPicture.asset("assets/icons/back.svg"), + TextButton( + onPressed: () { + Navigator.pop(context); + }, + child: SvgPicture.asset("assets/icons/back.svg")), SvgPicture.asset("assets/images/interests_progress.svg"), SizedBox(height: 40.0), Text("What are your interests?", @@ -110,31 +111,23 @@ class _InterestsPageWidgetState extends State { TextButton( onPressed: () async { if (_formKey.currentState!.validate()) { - assert(widget.user != null || widget.idToken != null); - final auth = await widget.user?.authentication; - final idToken = - widget.user != null ? auth?.idToken : widget.idToken; - final endpoint_string = API_URL + - (widget.user != null ? "/google" : "/device-login"); - final connectionResult = await client.connect( - idToken!, - Uri.parse(endpoint_string), - this.widget.userType, + List interests = []; + for (int i = 0; i < _checked.length; i++) { + if (_checked[i]) interests.add(_categories[i]); + } + + final connectionResult = await client.connectGoogle( + widget.user!, this.widget.year ?? "", - this.widget.username); + this.widget.userType, + this.widget.username, + this.widget.college ?? "", + this.widget.major ?? "", + interests); if (connectionResult == null) { displayToast("An error occurred while signing you up!", Status.error); - } else { - //Connect to home page here. - print("Connection result:"); - print(connectionResult.body); - displayToast("Signed in!", Status.success); - Navigator.pushReplacement( - context, - MaterialPageRoute( - builder: (context) => BottomNavBar())); } } }, diff --git a/game/lib/journeys/filter_form.dart b/game/lib/journeys/filter_form.dart index c2ca7a6d..8bb8e5e3 100644 --- a/game/lib/journeys/filter_form.dart +++ b/game/lib/journeys/filter_form.dart @@ -1,30 +1,62 @@ import 'package:flutter/material.dart'; +import 'package:game/api/game_client_dto.dart'; +import 'package:game/utils/utility_functions.dart'; class FilterForm extends StatefulWidget { - const FilterForm({Key? key}) : super(key: key); + final void Function(List, List, String) onSubmit; + String? myDifficulty; + List? myLocations; + List? myCategories; + FilterForm( + {Key? key, + required this.onSubmit, + String? difficulty, + List? locations, + List? categories}) + : super(key: key) { + myDifficulty = difficulty; + myLocations = locations; + myCategories = categories; + } @override - State createState() => _FilterFormState(); + // State createState() => _FilterFormState(status); + State createState() { + return _FilterFormState(myDifficulty, myLocations, myCategories); + } } class _FilterFormState extends State { // Define variables for tracking the selected values List selectedCategories = []; List selectedLocations = []; - String selectedStatus = 'Easy'; + late String selectedDifficulty; + + _FilterFormState( + String? difficulty, List? locations, List? categories) { + selectedDifficulty = difficulty ?? ''; + selectedLocations = locations ?? []; + selectedCategories = categories ?? []; + } + + List categories = EventCategoryDto.values; + + List locations = ChallengeLocationDto.values; - List categories = [ - 'Food', - 'Nature', - 'Historical', - 'Cafe', - 'Dining Hall', - 'Dorm' - ]; - List locations = ['Location 1', 'Location 2', 'Location 3']; - List statuses = ['Easy', 'Medium', 'Hard']; + List difficulties = EventDifficultyDto.values; // Define methods for updating the selected values + void filterChallenges() { + // setState(() { + // selectedCategories; + // selectedLocations; + // selectedStatus; + // }); + widget.onSubmit(selectedCategories, selectedLocations, selectedDifficulty); + + Navigator.pop(context); + } + void toggleCategory(String category) { if (selectedCategories.contains(category)) { selectedCategories.remove(category); @@ -41,9 +73,9 @@ class _FilterFormState extends State { } } - void setStatus(String status) { + void setDifficulty(String diff) { setState(() { - selectedStatus = status; + selectedDifficulty = diff; }); } @@ -119,13 +151,14 @@ class _FilterFormState extends State { return ElevatedButton( onPressed: () { setState(() { - toggleCategory(category); + toggleCategory(category.name); }); }, style: ElevatedButton.styleFrom( - backgroundColor: selectedCategories.contains(category) - ? Color.fromARGB(255, 249, 237, 218) - : Color.fromARGB(100, 210, 210, 210), + backgroundColor: + selectedCategories.contains(category.name) + ? Color.fromARGB(255, 249, 237, 218) + : Color.fromARGB(100, 210, 210, 210), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(20.0), ), @@ -134,7 +167,7 @@ class _FilterFormState extends State { child: Padding( padding: EdgeInsets.symmetric(vertical: 8.0), child: Text( - category, + friendlyCategory[category] ?? '', style: TextStyle( fontSize: 16.0, color: Colors.black.withOpacity(0.6), @@ -162,13 +195,14 @@ class _FilterFormState extends State { return ElevatedButton( onPressed: () { setState(() { - toggleLocation(location); + toggleLocation(location.name); }); }, style: ElevatedButton.styleFrom( - backgroundColor: selectedLocations.contains(location) - ? Color.fromARGB(255, 249, 237, 218) - : Color.fromARGB(100, 210, 210, 210), + backgroundColor: + selectedLocations.contains(location.name) + ? Color.fromARGB(255, 249, 237, 218) + : Color.fromARGB(100, 210, 210, 210), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(20.0), ), @@ -177,7 +211,7 @@ class _FilterFormState extends State { child: Padding( padding: EdgeInsets.symmetric(vertical: 8.0), child: Text( - location, + friendlyLocation[location] ?? '', style: TextStyle( fontSize: 16.0, color: Colors.black.withOpacity(0.6), @@ -201,26 +235,27 @@ class _FilterFormState extends State { Wrap( spacing: 4, runSpacing: 4, - children: statuses.map((status) { + children: difficulties.map((diff) { return ElevatedButton( onPressed: () { setState(() { - setStatus(status); + setDifficulty(diff.name); }); }, style: ElevatedButton.styleFrom( - backgroundColor: selectedStatus.contains(status) - ? Color.fromARGB(255, 249, 237, 218) - : Color.fromARGB(100, 210, 210, 210), + backgroundColor: + selectedDifficulty.contains(diff.name) + ? Color.fromARGB(255, 249, 237, 218) + : Color.fromARGB(100, 210, 210, 210), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(20.0), ), elevation: 0, ), child: Padding( - padding: EdgeInsets.symmetric(vertical: 8.0), + padding: EdgeInsets.symmetric(vertical: 1.0), child: Text( - status, + friendlyDifficulty[diff] ?? '', style: TextStyle( fontSize: 16.0, color: Colors.black.withOpacity(0.6), @@ -233,7 +268,7 @@ class _FilterFormState extends State { ], ), ), - Padding(padding: EdgeInsets.symmetric(vertical: 40.0)), + Padding(padding: EdgeInsets.symmetric(vertical: 20.0)), Container( width: 600, decoration: ShapeDecoration( @@ -246,7 +281,7 @@ class _FilterFormState extends State { ), ), ), - Padding(padding: EdgeInsets.symmetric(vertical: 20.0)), + Padding(padding: EdgeInsets.symmetric(vertical: 5.0)), Row( // padding: EdgeInsets.symmetric(vertical: 30), mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -258,7 +293,7 @@ class _FilterFormState extends State { setState(() { selectedCategories = []; selectedLocations = []; - selectedStatus = 'All'; + selectedDifficulty = ''; }); }, child: Text('Clear'), @@ -274,9 +309,7 @@ class _FilterFormState extends State { Padding( padding: EdgeInsets.symmetric(horizontal: 20.0), child: TextButton( - onPressed: () { - // TODO: Implement apply filters button - }, + onPressed: filterChallenges, child: Text('See results'), style: ElevatedButton.styleFrom( foregroundColor: Color.fromARGB(255, 255, 255, 255), diff --git a/game/lib/journeys/journey_cell.dart b/game/lib/journeys/journey_cell.dart index 0925ca79..eea479be 100644 --- a/game/lib/journeys/journey_cell.dart +++ b/game/lib/journeys/journey_cell.dart @@ -1,11 +1,14 @@ import 'package:flutter/material.dart'; import 'package:game/preview/preview.dart'; +import 'package:flutter_svg/flutter_svg.dart'; class JourneyCell extends StatefulWidget { final int locationCount; final String location; final String journeyName; - final Image thumbnail; + final double? challengeLong; + final double? challengeLat; + final String imgUrl; final String description; final int numberCompleted; final bool isCompleted; @@ -15,8 +18,10 @@ class JourneyCell extends StatefulWidget { const JourneyCell( this.journeyName, + this.challengeLat, + this.challengeLong, this.location, - this.thumbnail, + this.imgUrl, this.description, this.locationCount, this.numberCompleted, @@ -30,8 +35,10 @@ class JourneyCell extends StatefulWidget { @override State createState() => _JourneyCellState( journeyName, + challengeLat, + challengeLong, location, - thumbnail, + imgUrl, description, locationCount, numberCompleted, @@ -44,8 +51,10 @@ class JourneyCell extends StatefulWidget { class _JourneyCellState extends State { final int locationCount; final String journeyName; + final double? challengeLong; + final double? challengeLat; final String location; - final Image thumbnail; + final String imgUrl; final String description; final int numberCompleted; final bool isCompleted; @@ -56,8 +65,10 @@ class _JourneyCellState extends State { _JourneyCellState( this.journeyName, + this.challengeLat, + this.challengeLong, this.location, - this.thumbnail, + this.imgUrl, this.description, this.locationCount, this.numberCompleted, @@ -71,286 +82,238 @@ class _JourneyCellState extends State { @override Widget build(BuildContext context) { return GestureDetector( - onTap: () async { - await showModalBottomSheet( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.vertical(top: Radius.circular(10.0)), - ), - context: context, - isScrollControlled: true, - builder: ( - BuildContext context, - ) => - Preview(journeyName, description, difficulty, points, - PreviewType.JOURNEY, location, id, - locationCount: locationCount, - numberCompleted: numberCompleted)); - }, - child: Container( - decoration: BoxDecoration( - color: Colors.white, - border: Border.all( - color: Color.fromARGB(255, 255, 255, 255), + onTap: () async { + await showModalBottomSheet( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(10.0)), ), - borderRadius: BorderRadius.circular(20), - boxShadow: [ - BoxShadow( - color: Colors.grey.withOpacity(0.5), - spreadRadius: 1, - blurRadius: 2, - offset: Offset(0, 2), - ), - ], + context: context, + isScrollControlled: true, + builder: ( + BuildContext context, + ) => + Preview( + journeyName, + challengeLat, + challengeLong, + description, + imgUrl, + difficulty, + points, + PreviewType.JOURNEY, + location, + id, + locationCount: locationCount, + numberCompleted: numberCompleted)); + }, + child: Container( + width: MediaQuery.sizeOf(context).width * 0.9, + height: MediaQuery.sizeOf(context).height * 0.3, + decoration: BoxDecoration( + color: Colors.white, + border: Border.all( + color: Color.fromARGB(255, 255, 255, 255), ), - height: 236.0, - width: 345, - child: Padding( - padding: EdgeInsets.all(3.0), - child: Column( - mainAxisAlignment: MainAxisAlignment.end, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Padding( - // padding: const EdgeInsets.only(right: 2), - Container( - width: double.infinity, - // width: MediaQuery.of(context).size.width, - height: 113, - decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.5), + spreadRadius: 1, + blurRadius: 2, + offset: Offset(0, 2), + ), + ], + ), + child: Stack( + alignment: Alignment.topCenter, + children: [ + Container( + width: double.infinity, + height: MediaQuery.sizeOf(context).height * 0.15, + decoration: BoxDecoration( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(10), + topRight: Radius.circular(10)), + image: DecorationImage( + fit: BoxFit.cover, + image: NetworkImage(imgUrl), + ), + )), + Positioned( + top: 10.0, + left: 16.0, + child: Container( + height: 31.58, + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), + decoration: ShapeDecoration( + color: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Image.asset( + "assets/images/locationVector.png", + alignment: Alignment.centerLeft, + ), + Padding(padding: EdgeInsets.symmetric(horizontal: 5)), + Text( + widget.location, + style: TextStyle( + color: Color(0xFF835A7C), + fontSize: 16, + fontFamily: 'Poppins', + fontWeight: FontWeight.w400, + height: 0, + ), + ), + ], + ), + ), + ), + Positioned( + bottom: 0, + left: 0, + child: Container( + width: MediaQuery.sizeOf(context).width, + decoration: ShapeDecoration( + color: Colors.white, + shape: RoundedRectangleBorder( borderRadius: BorderRadius.only( - topLeft: Radius.circular(20), - topRight: Radius.circular(20)), - image: DecorationImage( - fit: BoxFit.fill, - image: - NetworkImage("https://picsum.photos/250?image=9"), + bottomLeft: Radius.circular(10), + bottomRight: Radius.circular(10))), + ), + padding: + const EdgeInsets.symmetric(horizontal: 17.0, vertical: 8), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + widget.journeyName, + style: TextStyle( + color: Colors.black, + fontSize: 20, + fontFamily: 'Poppins', + fontWeight: FontWeight.w800, ), - ) - // alignment: - // child: [ - // ClipRRect( - // borderRadius: BorderRadius.all(Radius.circular(4.6)), - // child: thumbnail, - // ), - // ] - ), - if (isCompleted) - Flexible( - child: Expanded( - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Text( - // "COMPLETED", - // style: TextStyle( - // color: Color.fromARGB(255, 71, 71, 71), - // fontSize: 10, - // fontWeight: FontWeight.w700, - // fontFamily: 'Lato', - // ), - // ), - // Column( - // children: [ - // FractionallySizedBox( - // alignment: Alignment.topLeft, - // widthFactor: (locationCount > 0 - // ? numberCompleted / locationCount - // : 0), - // child: Container( - // color: Color.fromARGB(255, 0, 0, 0)), - // ), - // Icon(Icons.location_on, - // size: 20, color: Colors.purple), - // Text( - // numberCompleted.toString(), - // style: TextStyle( - // color: Color.fromARGB(179, 0, 0, 0), - // fontSize: 12, - // fontFamily: 'Lato', - // ), - // ), - // ], - // ), - ], + Text( + widget.description, + style: TextStyle( + color: Colors.black, + fontSize: 14, + fontFamily: 'Poppins', + fontWeight: FontWeight.w400, ), ), - ), - SizedBox( - height: 5, - ), - Container( - height: 24, - child: Row( - mainAxisAlignment: MainAxisAlignment.start, + SizedBox(height: 5), + Row( children: [ - Icon(Icons.tour, - size: 15, color: Color.fromARGB(255, 131, 90, 124)), - Text( - location, - style: TextStyle( - color: Color.fromARGB(255, 131, 90, 124), - fontSize: 16, - fontFamily: 'Poppins', - ), - ), - Spacer(), - Container( - padding: - EdgeInsets.symmetric(horizontal: 10, vertical: 3), - decoration: BoxDecoration( - color: Color.fromARGB(255, 249, 237, 218), - borderRadius: BorderRadius.circular(20), - ), - child: Text( - difficulty, - style: TextStyle( - color: Color.fromARGB(204, 0, 0, 0), - fontSize: 14, - fontFamily: 'Poppins', - fontWeight: FontWeight.w600, - ), - ), - ), - SizedBox(width: 10), // Add spacing between buttons Container( - padding: - EdgeInsets.symmetric(horizontal: 10, vertical: 2), - decoration: BoxDecoration( - border: Border.all( - color: Color.fromARGB(255, 255, 199, 55), + width: 70, + height: 25, + padding: const EdgeInsets.symmetric( + horizontal: 2, vertical: 2), + decoration: ShapeDecoration( + color: Color(0xFFF9EDDA), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), ), - color: Color.fromARGB(255, 189, 135, 31), - borderRadius: BorderRadius.circular(20), ), - child: Text( - points.toString() + "PTS", - style: TextStyle( - color: Colors.white, - fontSize: 14, - fontFamily: 'Poppins', - fontWeight: FontWeight.w600, + child: Center( + child: Text( + widget.difficulty, + style: TextStyle( + fontSize: 14, + fontFamily: 'Poppins', + fontWeight: FontWeight.w400, + height: 0, + ), ), ), ), + SizedBox(width: 8), + Row(children: [ + SvgPicture.asset( + "assets/icons/bearcoins.svg", + width: 25, + ), + Text(' ' + points.toString() + " PTS", + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: Color(0xFFC17E19))) + ]), ], - )), - SizedBox( - height: 5, - ), - Container( - height: 25, - child: Text( - journeyName, - style: TextStyle( - color: Color.fromARGB(255, 0, 0, 0), - fontSize: 16, - fontFamily: 'Poppins', - fontWeight: FontWeight.bold), - ), - ), - Container( - height: 21, - child: Text( - description, - style: TextStyle( - color: Color.fromARGB(179, 0, 0, 0), - fontSize: 14, - fontFamily: 'Poppins', ), - ), - ), - Padding( - padding: const EdgeInsets.only(top: 9), - child: Row( - children: [ - Stack(children: [ - Container( - width: 260, - height: 20, - alignment: Alignment.centerLeft, - child: Container( - decoration: new BoxDecoration( - color: Color.fromARGB(255, 241, 241, 241), - shape: BoxShape.rectangle, - borderRadius: - BorderRadius.all(Radius.circular(16.0)), - ), - ), - ), - Container( - width: (locationCount > 0 - ? numberCompleted / locationCount - : 0) * - 260, - height: 20, - alignment: Alignment.centerLeft, - child: Container( - decoration: new BoxDecoration( - color: Color.fromARGB(191, 237, 86, 86), - shape: BoxShape.rectangle, - borderRadius: - BorderRadius.all(Radius.circular(16.0)), + Padding( + padding: const EdgeInsets.only(top: 15.0, bottom: 5), + child: Row( + children: [ + Stack(children: [ + Container( + width: MediaQuery.sizeOf(context).width * 0.66, + height: 22, + alignment: Alignment.centerLeft, + child: Container( + decoration: new BoxDecoration( + color: Color.fromARGB(255, 241, 241, 241), + shape: BoxShape.rectangle, + borderRadius: + BorderRadius.all(Radius.circular(16.0)), + ), + ), ), - ), - ), - ]), - // Container( - // width: 260, - // child: ClipRRect( - // borderRadius: BorderRadius.circular(10), - // child: Container( - // height: 20.0, - // width: 10.0, - // color: Color.fromARGB(255, 255, 255, 255), - // child: FractionallySizedBox( - // alignment: Alignment.topLeft, - // widthFactor: (locationCount > 0 - // ? numberCompleted / locationCount - // : 0), - // child: Container( - // color: Color.fromARGB(191, 237, 86, 86)), - // ), - // ), - // ), - // ), - SizedBox(width: 8), - Container( - width: 50, - child: Row(children: [ - Icon(Icons.location_on, - size: 16, - color: Color.fromARGB(255, 131, 90, 124)), - Text( - numberCompleted.toString() + - "/" + - locationCount.toString(), - style: TextStyle( - color: Color.fromARGB(255, 110, 110, 110), - fontSize: 16, - fontFamily: 'Poppins', + Container( + width: (locationCount > 0 + ? numberCompleted / locationCount + : 0) * + MediaQuery.sizeOf(context).width * + 0.66, + height: 20, + alignment: Alignment.centerLeft, + child: Container( + decoration: new BoxDecoration( + color: Color.fromARGB(191, 237, 86, 86), + shape: BoxShape.rectangle, + borderRadius: + BorderRadius.all(Radius.circular(16.0)), + ), + ), ), - ), - ]), - ) - // Container( - // height: 3, - // child: Text( - // numberCompleted.toString(), - // style: TextStyle( - // color: Color.fromARGB(255, 0, 0, 0), - // fontSize: 18, - // fontFamily: 'Lato', - // ), - // ), - // ), - ], - ), + ]), + SizedBox(width: 8), + Container( + width: 50, + child: Row(children: [ + SvgPicture.asset("assets/icons/pin.svg"), + Text( + " " + + numberCompleted.toString() + + "/" + + locationCount.toString(), + style: TextStyle( + color: Color.fromARGB(255, 110, 110, 110), + fontSize: 16, + fontWeight: FontWeight.w600, + fontFamily: 'Poppins', + ), + ), + ]), + ) + ], + ), + ), + ], ), - ], + ), ), - ), - )); + ], + ), + ), + ); } } diff --git a/game/lib/journeys/journeys_page.dart b/game/lib/journeys/journeys_page.dart index 2bc6d174..1665a202 100644 --- a/game/lib/journeys/journeys_page.dart +++ b/game/lib/journeys/journeys_page.dart @@ -9,10 +9,38 @@ import 'package:game/model/event_model.dart'; import 'package:game/model/group_model.dart'; import 'package:game/model/tracker_model.dart'; import 'package:game/model/challenge_model.dart'; -import 'package:game/model/user_model.dart'; import 'package:game/utils/utility_functions.dart'; import 'package:provider/provider.dart'; +class JourneyCellDto { + JourneyCellDto({ + required this.location, + required this.name, + required this.lat, + required this.long, + required this.imgUrl, + required this.complete, + required this.locationCount, + required this.numberCompleted, + required this.description, + required this.difficulty, + required this.points, + required this.eventId, + }); + late String location; + late String name; + late double? lat; + late double? long; + late String imgUrl; + late bool complete; + late int locationCount; + late int numberCompleted; + late String description; + late String difficulty; + late int points; + late String eventId; +} + class JourneysPage extends StatefulWidget { const JourneysPage({Key? key}) : super(key: key); @@ -21,6 +49,12 @@ class JourneysPage extends StatefulWidget { } class _JourneysPageState extends State { + List selectedCategories = []; + List selectedLocations = []; + String selectedStatus = ''; + + List eventData = []; + void openFilter() { showModalBottomSheet( context: context, @@ -28,10 +62,19 @@ class _JourneysPageState extends State { builder: ( BuildContext context, ) { - return FilterForm(); + return FilterForm(onSubmit: handleFilterSubmit); }); } + // Callback function to receive updated state values from the child + void handleFilterSubmit(List a, List b, String c) { + setState(() { + selectedCategories = a; + selectedLocations = b; + selectedStatus = c; + }); + } + @override Widget build(BuildContext context) { return Scaffold( @@ -44,177 +87,186 @@ class _JourneysPageState extends State { child: Center( child: Padding( padding: EdgeInsets.all(16), - child: Column( + child: Stack( children: [ - Container( - height: 30, - color: Color.fromARGB(51, 217, 217, 217), - child: TextField( - decoration: InputDecoration( - prefixIcon: Icon( - Icons.search, - color: Color.fromARGB(204, 0, 0, 0), - size: 12, - ), - border: OutlineInputBorder( - borderRadius: BorderRadius.all(Radius.circular(1.0))), - labelText: "Search a challenge name, location, etc...", - labelStyle: TextStyle( - color: Color.fromARGB(76, 0, 0, 0), - fontSize: 12, - fontFamily: 'Lato', - ), - ), + Align( + alignment: Alignment.bottomCenter, + child: Image( + image: AssetImage('assets/images/go-logo.png'), + width: MediaQuery.of(context).size.width / 3, + height: MediaQuery.of(context).size.height / 3, ), ), - Container( - child: Padding( - padding: const EdgeInsets.only(top: 24.0, bottom: 24.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Container( - height: 30, - child: TextButton.icon( - onPressed: openFilter, - icon: Icon( - Icons.filter_list_rounded, - color: Color.fromARGB(255, 0, 0, 0), - size: 20.0, - ), - style: ButtonStyle( - backgroundColor: - MaterialStateProperty.all( - Color.fromARGB(153, 217, 217, 217)), - padding: MaterialStateProperty.all( - EdgeInsets.only(right: 16.0, left: 16.0), - ), - shape: MaterialStateProperty.all( - RoundedRectangleBorder( - borderRadius: BorderRadius.circular(3.0), - )), - ), - label: Text( - "Filter By", - style: TextStyle( - color: Color.fromARGB(255, 0, 0, 0), - fontSize: 15, - fontFamily: 'Inter', - ), - )), + Column( + children: [ + Container( + height: 30, + color: Color.fromARGB(51, 217, 217, 217), + child: TextField( + decoration: InputDecoration( + prefixIcon: Icon( + Icons.search, + color: Color.fromARGB(204, 0, 0, 0), + size: 12, + ), + border: OutlineInputBorder( + borderRadius: + BorderRadius.all(Radius.circular(1.0))), + labelText: + "Search a challenge name, location, etc...", + labelStyle: TextStyle( + color: Color.fromARGB(76, 0, 0, 0), + fontSize: 12, + fontFamily: 'Lato', + ), ), - ], + ), ), - ), - ), - Expanded(child: Consumer4( - builder: (context, myEventModel, groupModel, trackerModel, - challengeModel, child) { - List eventCells = []; - if (myEventModel.searchResults == null) { - myEventModel.searchEvents( - 0, - 1000, - [ - EventTimeLimitationDto.PERPETUAL, - EventTimeLimitationDto.LIMITED_TIME - ], - false, - false, - false); - } - final events = myEventModel.searchResults ?? []; - if (!events - .any((element) => element.id == groupModel.curEventId)) { - final curEvent = - myEventModel.getEventById(groupModel.curEventId ?? ""); - if (curEvent != null) events.add(curEvent); - } - for (EventDto event in events) { - var tracker = trackerModel.trackerByEventId(event.id); - var numberCompleted = tracker?.prevChallenges?.length ?? 0; - var complete = - (numberCompleted == event.challenges?.length); - var locationCount = event.challenges?.length ?? 0; + Container( + child: Padding( + padding: const EdgeInsets.only(top: 24.0, bottom: 24.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Container( + height: 30, + child: TextButton.icon( + onPressed: openFilter, + icon: Icon( + Icons.filter_list_rounded, + color: Color.fromARGB(255, 0, 0, 0), + size: 20.0, + ), + style: ButtonStyle( + backgroundColor: + MaterialStateProperty.all( + Color.fromARGB(153, 217, 217, 217)), + padding: MaterialStateProperty.all( + EdgeInsets.only(right: 16.0, left: 16.0), + ), + shape: MaterialStateProperty.all( + RoundedRectangleBorder( + borderRadius: BorderRadius.circular(3.0), + )), + ), + label: Text( + "Filter By", + style: TextStyle( + color: Color.fromARGB(255, 0, 0, 0), + fontSize: 15, + fontFamily: 'Inter', + ), + )), + ), + ], + ), + ), + ), + Expanded(child: Consumer5( + builder: (context, myEventModel, groupModel, + trackerModel, challengeModel, apiClient, child) { + if (myEventModel.searchResults == null) { + myEventModel.searchEvents( + 0, + 1000, + [ + EventTimeLimitationDto.PERPETUAL, + EventTimeLimitationDto.LIMITED_TIME + ], + false, + false, + false); + } + final events = myEventModel.searchResults ?? []; + if (!events.any( + (element) => element.id == groupModel.curEventId)) { + final curEvent = myEventModel + .getEventById(groupModel.curEventId ?? ""); + if (curEvent != null) events.add(curEvent); + } + eventData.clear(); - if (locationCount < 2) continue; - var totalPoints = 0; + for (EventDto event in events) { + var tracker = trackerModel.trackerByEventId(event.id); + var numberCompleted = + tracker?.prevChallenges.length ?? 0; + var complete = + (numberCompleted == event.challenges?.length); + var locationCount = event.challenges?.length ?? 0; - var challenge = challengeModel - .getChallengeById(event.challenges?[0] ?? ""); + if (locationCount < 2) continue; + var totalPoints = 0; - if (challenge == null) continue; - var location = challenge.location; + var challenge = challengeModel + .getChallengeById(event.challenges?[0] ?? ""); - for (var challengeId in event.challenges ?? []) { - var challenge = - challengeModel.getChallengeById(challengeId); - if (challenge != null) { - totalPoints += challenge.points ?? 0; - } - } - var difficulty = event.difficulty; - DateTime now = DateTime.now(); - DateTime endtime = HttpDate.parse(event.endTime ?? ""); + if (challenge == null) continue; + var location = challenge.location?.name; + var imageUrl = challenge.imageUrl; - Duration timeTillExpire = endtime.difference(now); - if (!complete) - eventCells.add( - StreamBuilder( - stream: - Stream.fromFuture(Future.delayed(timeTillExpire)), - builder: (stream, value) => timeTillExpire.isNegative - ? Consumer( - builder: (context, apiClient, child) { - if (event.id == groupModel.curEventId) { - apiClient.serverApi?.setCurrentEvent( - SetCurrentEventDto(eventId: "")); - } - return Container(); - }, - ) - : JourneyCell( - key: UniqueKey(), - event.name ?? "", - friendlyLocation[location] ?? "", - Image.network( - "https://picsum.photos/250?image=9"), // dummy data for now; should pass in thumbnail parameter - event.description ?? "", - locationCount, - numberCompleted, - complete, - friendlyDifficulty[difficulty] ?? "", - totalPoints, event.id, - ), - ), - ); - } - return ListView.separated( - padding: const EdgeInsets.symmetric(horizontal: 3), - itemCount: eventCells.length + 1, - itemBuilder: (context, index) { - if (index == eventCells.length) { - // Footer widget - return Padding( - padding: const EdgeInsets.only(bottom: 50.0), - child: Center( - child: Image( - image: AssetImage('assets/images/go-logo.png'), - width: 200, - height: 200, - ), - )); + for (var challengeId in event.challenges ?? []) { + var challenge = + challengeModel.getChallengeById(challengeId); + if (challenge != null) { + totalPoints += challenge.points ?? 0; + } + } + DateTime now = DateTime.now(); + DateTime endtime = HttpDate.parse(event.endTime ?? ""); + + Duration timeTillExpire = endtime.difference(now); + if (!complete && !timeTillExpire.isNegative) { + eventData.add(JourneyCellDto( + location: + friendlyLocation[challenge.location] ?? "", + name: event.name ?? "", + lat: challenge.latF ?? null, + long: challenge.longF ?? null, + imgUrl: imageUrl ?? + "https://upload.wikimedia.org/wikipedia/commons/b/b1/Missing-image-232x150.png", + complete: complete, + locationCount: locationCount, + numberCompleted: numberCompleted, + description: event.description ?? "", + difficulty: + friendlyDifficulty[event.difficulty] ?? "", + points: totalPoints, + eventId: event.id, + )); + } else if (event.id == groupModel.curEventId) { + apiClient.serverApi?.setCurrentEvent( + SetCurrentEventDto(eventId: "")); + } } - return eventCells[index]; - }, - physics: BouncingScrollPhysics(), - separatorBuilder: (context, index) { - return SizedBox(height: 10); - }, - ); - })) + return ListView.separated( + padding: const EdgeInsets.symmetric(horizontal: 3), + itemCount: eventData.length, + itemBuilder: (context, index) { + return JourneyCell( + key: UniqueKey(), + eventData[index].name, + eventData[index].lat, + eventData[index].long, + eventData[index].location, + eventData[index].imgUrl, + eventData[index].description, + eventData[index].locationCount, + eventData[index].numberCompleted, + eventData[index].complete, + eventData[index].difficulty, + eventData[index].points, + eventData[index].eventId); + }, + physics: BouncingScrollPhysics(), + separatorBuilder: (context, index) { + return SizedBox(height: 10); + }, + ); + })) + ], + ), ], ), ), diff --git a/game/lib/loading_page/loading_page.dart b/game/lib/loading_page/loading_page.dart new file mode 100644 index 00000000..c93a31bd --- /dev/null +++ b/game/lib/loading_page/loading_page.dart @@ -0,0 +1,41 @@ +import 'package:flutter/material.dart'; +import 'package:game/navigation_page/bottom_navbar.dart'; +import 'package:game/splash_page/splash_page.dart'; + +class LoadingPageWidget extends StatelessWidget { + final Future relogResult; + LoadingPageWidget(this.relogResult, {Key? key}) : super(key: key); + final scaffoldKey = GlobalKey(); + + Future awaitRelogResult() async { + final result = await relogResult; + // Wait, so we can allow the serverApi to notifyListeners() first + await Future.delayed(Durations.medium1); + return result; + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.white, + body: Center( + child: StreamBuilder( + stream: Stream.fromFuture(awaitRelogResult()), + builder: (context, snapshot) { + if (snapshot.hasData) { + WidgetsBinding.instance.addPostFrameCallback((_) { + Navigator.pushReplacement( + context, + MaterialPageRoute( + builder: (context) => snapshot.data! + ? BottomNavBar() + : SplashPageWidget())); + }); + } + + return CircularProgressIndicator(); + }, + ), + )); + } +} diff --git a/game/lib/main.dart b/game/lib/main.dart index 8fd3ad56..68fca21f 100644 --- a/game/lib/main.dart +++ b/game/lib/main.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:flutter_config/flutter_config.dart'; +import 'package:game/loading_page/loading_page.dart'; // imports for google maps import 'dart:io' show Platform; @@ -11,28 +12,15 @@ import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platf // api and widget imports import 'package:game/api/game_api.dart'; -import 'package:game/gameplay/gameplay_page.dart'; -import 'package:game/gameplay/gameplay_map.dart'; -import 'package:game/global_leaderboard/global_leaderboard_widget.dart'; -import 'package:game/journeys/journeys_page.dart'; import 'package:game/model/challenge_model.dart'; import 'package:game/model/event_model.dart'; import 'package:game/model/group_model.dart'; import 'package:game/model/tracker_model.dart'; import 'package:game/model/user_model.dart'; -import 'package:game/profile/achievement_cell.dart'; -import 'package:game/profile/completed_cell.dart'; -import 'package:game/profile/profile_page.dart'; -import 'package:game/profile/settings_page.dart'; -import 'package:game/register_page/register_page.dart'; -import 'package:game/navigation_page/bottom_navbar.dart'; -import 'package:game/splash_page/splash_page.dart'; import 'package:game/widget/game_widget.dart'; import 'package:provider/provider.dart'; import 'package:game/color_palette.dart'; -import 'dart:io' show Platform; - const ENV_URL = String.fromEnvironment('API_URL', defaultValue: ""); final storage = FlutterSecureStorage(); @@ -45,7 +33,7 @@ void main() async { final GoogleMapsFlutterPlatform platform = GoogleMapsFlutterPlatform.instance; // should only apply to Android - needs to be tested for iOS if (platform is GoogleMapsFlutterAndroid) { - (platform as GoogleMapsFlutterAndroid).useAndroidViewSurface = true; + (platform).useAndroidViewSurface = true; initializeMapRenderer(); } // load environment variables @@ -113,20 +101,17 @@ class MyApp extends StatelessWidget { ], child: GameWidget( child: MaterialApp( - title: 'CornellGO!', - localizationsDelegates: [ - GlobalMaterialLocalizations.delegate, - GlobalWidgetsLocalizations.delegate, - GlobalCupertinoLocalizations.delegate, - ], - supportedLocales: const [Locale('en', '')], - theme: ThemeData( - fontFamily: 'Poppins', primarySwatch: ColorPalette.BigRed), - home: StreamBuilder( - stream: Stream.fromFuture(client.tryRelog()), - builder: (stream, snapshot) => (snapshot.data == null) - ? Container() - : (snapshot.data! ? BottomNavBar() : SplashPageWidget())), - ))); + title: 'CornellGO!', + localizationsDelegates: [ + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: const [Locale('en', '')], + theme: ThemeData( + fontFamily: 'Poppins', + primarySwatch: ColorPalette.BigRed, + useMaterial3: false), + home: LoadingPageWidget(client.tryRelog())))); } } diff --git a/game/lib/model/achievement_model.dart b/game/lib/model/achievement_model.dart new file mode 100644 index 00000000..c7c7bbb8 --- /dev/null +++ b/game/lib/model/achievement_model.dart @@ -0,0 +1,30 @@ +import 'package:flutter/foundation.dart'; +import 'package:game/api/game_api.dart'; +import 'package:game/api/game_client_dto.dart'; + +/** + * This file represents the model for the achievements. Whenever a achievement is updated, added or deleted from the backend, the model is updated and notifies the Consumer so that the front end can be modified. + */ +class AchievementModel extends ChangeNotifier { + Map _achievementsById = {}; + ApiClient _client; + + AchievementModel(ApiClient client) : _client = client { + /** + * Stream that listens to updates on the achievements. + */ + client.clientApi.updateAchievementDataStream.listen((event) { + if (event.deleted) { + _achievementsById.remove(event.achievement.id); + } else { + _achievementsById[event.achievement.id] = event.achievement; + } + notifyListeners(); + }); + + client.clientApi.connectedStream.listen((event) { + _achievementsById.clear(); + notifyListeners(); + }); + } +} diff --git a/game/lib/model/event_model.dart b/game/lib/model/event_model.dart index c19b937d..519318cf 100644 --- a/game/lib/model/event_model.dart +++ b/game/lib/model/event_model.dart @@ -36,14 +36,60 @@ class EventModel extends ChangeNotifier { if (event.users.length == 0) { return; } - final players = _topPlayers[event.eventId]; + var players = _topPlayers[event.eventId]; + if (players == null) { + players = []; + _topPlayers[event.eventId ?? ""] = players; + } + for (int i = event.offset; i < event.users.length; i++) { - if (i < players!.length) { + if (i < players.length) { players[i] = event.users[i - event.offset]; } else { players.add(event.users[i - event.offset]); } } + + notifyListeners(); + }); + + client.clientApi.updateUserDataStream.listen((event) { + if (event.user.username == null) return; + + for (final playerList in _topPlayers.values) { + for (final player in playerList) { + if (player.userId == event.user.id) { + player.username = event.user.username!; + break; + } + } + } + + notifyListeners(); + }); + + client.clientApi.updateLeaderPositionStream.listen((event) { + final forEvent = _topPlayers[event.eventId]; + final forGlobal = _topPlayers[""]; + + if (forEvent != null) { + forEvent + .firstWhere((element) => element.userId == event.playerId, + orElse: () => LeaderDto(userId: "", username: "", score: 0)) + .score = event.newEventScore; + + forEvent.sort((a, b) => b.score.compareTo(a.score)); + } + + if (forGlobal != null) { + forGlobal + .firstWhere((element) => element.userId == event.playerId, + orElse: () => LeaderDto(userId: "", username: "", score: 0)) + .score = event.newTotalScore; + + forGlobal.sort((a, b) => b.score.compareTo(a.score)); + } + notifyListeners(); }); @@ -55,23 +101,14 @@ class EventModel extends ChangeNotifier { }); } - List getTopPlayersForEvent(String eventId, int count) { - final topPlayers = _topPlayers[eventId]; + List? getTopPlayersForEvent(String? eventId, int count) { + final topPlayers = _topPlayers[eventId ?? ""]; final diff = count - (topPlayers?.length ?? 0); if (topPlayers == null) { - _topPlayers[eventId] = []; - } - if (_topPlayers[eventId]?.length == 0) { - eventId.isEmpty - ? _client.serverApi?.requestGlobalLeaderData( - RequestGlobalLeaderDataDto( - offset: (topPlayers?.length ?? 0), count: 1000)) - : _client.serverApi?.requestEventLeaderData(RequestEventLeaderDataDto( - offset: (topPlayers?.length ?? 0), - count: diff, - eventId: eventId)); + _client.serverApi?.requestEventLeaderData(RequestEventLeaderDataDto( + offset: (topPlayers?.length ?? 0), count: diff, eventId: eventId)); } - return topPlayers ?? []; + return topPlayers; } EventDto? getEventById(String id) { diff --git a/game/lib/model/user_model.dart b/game/lib/model/user_model.dart index bb489aba..ca51ec7b 100644 --- a/game/lib/model/user_model.dart +++ b/game/lib/model/user_model.dart @@ -7,8 +7,9 @@ import 'package:game/api/game_client_dto.dart'; */ class UserModel extends ChangeNotifier { UserDto? userData; + ApiClient _client; - UserModel(ApiClient client) { + UserModel(ApiClient client) : _client = client { /** * Stream that listens to updates on the user data. */ @@ -24,4 +25,16 @@ class UserModel extends ChangeNotifier { client.serverApi?.requestUserData(RequestUserDataDto()); }); } + + void updateUserData(String id, String? username, String? college, + String? major, String? year) { + _client.serverApi?.updateUserData(UpdateUserDataDto( + user: UserDto( + id: id, + username: username, + college: college, + major: major, + year: year), + deleted: false)); + } } diff --git a/game/lib/navigation_page/bottom_navbar.dart b/game/lib/navigation_page/bottom_navbar.dart index 448f6eb0..18ad6827 100644 --- a/game/lib/navigation_page/bottom_navbar.dart +++ b/game/lib/navigation_page/bottom_navbar.dart @@ -1,7 +1,11 @@ import 'package:flutter/material.dart'; +import 'package:game/api/game_api.dart'; import 'package:game/global_leaderboard/global_leaderboard_widget.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:game/profile/profile_page.dart'; +import 'package:game/splash_page/splash_page.dart'; +import 'package:game/utils/utility_functions.dart'; +import 'package:provider/provider.dart'; import 'home_navbar.dart'; class BottomNavBar extends StatefulWidget { @@ -29,9 +33,26 @@ class _BottomNavBarState extends State { @override Widget build(BuildContext context) { + final client = Provider.of(context); + return Scaffold( body: Center( - child: _widgetOptions.elementAt(_selectedIndex), + child: StreamBuilder( + stream: client.clientApi.disconnectedStream, + builder: (context, snapshot) { + if (client.serverApi == null) { + print("ServerApi == null"); + WidgetsBinding.instance.addPostFrameCallback((_) { + Navigator.pushReplacement( + context, + MaterialPageRoute( + builder: (context) => SplashPageWidget())); + displayToast("Signed out", Status.success); + }); + } + + return _widgetOptions.elementAt(_selectedIndex); + }), ), bottomNavigationBar: BottomNavigationBar( items: [ diff --git a/game/lib/preview/preview.dart b/game/lib/preview/preview.dart index 19b8bb6c..3fcda2ac 100644 --- a/game/lib/preview/preview.dart +++ b/game/lib/preview/preview.dart @@ -4,6 +4,11 @@ import 'package:game/gameplay/gameplay_page.dart'; import 'package:provider/provider.dart'; import 'package:game/api/game_client_dto.dart'; +import 'package:geolocator/geolocator.dart'; +import 'package:game/api/geopoint.dart'; +import 'dart:async'; +import 'package:flutter_svg/flutter_svg.dart'; + enum PreviewType { CHALLENGE, JOURNEY } /** Returns a preview of a challenge given the challenge name, description, @@ -11,11 +16,13 @@ enum PreviewType { CHALLENGE, JOURNEY } * both Challenges and Journeys. */ class Preview extends StatefulWidget { final String challengeName; + final double? challengeLong; + final double? challengeLat; final String description; + final String imgUrl; final String difficulty; final int points; final PreviewType type; - final int locationCount; final int numberCompleted; final String location; @@ -32,8 +39,17 @@ class Preview extends StatefulWidget { //Temporary image for now. Will have to change later final String imgPath = "assets/images/38582.jpg"; - Preview(this.challengeName, this.description, this.difficulty, this.points, - this.type, this.location, this.eventId, + Preview( + this.challengeName, + this.challengeLat, + this.challengeLong, + this.description, + this.imgUrl, + this.difficulty, + this.points, + this.type, + this.location, + this.eventId, {this.locationCount = 1, this.numberCompleted = 0, // required this.totalDistance, @@ -43,7 +59,10 @@ class Preview extends StatefulWidget { @override State createState() => _PreviewState( challengeName, + challengeLat, + challengeLong, description, + imgUrl, difficulty, points, type, @@ -60,7 +79,10 @@ class Preview extends StatefulWidget { * challenge_on button */ class _PreviewState extends State { final String challengeName; + final double? challengeLong; + final double? challengeLat; final String description; + final String imgUrl; final String difficulty; final int points; final PreviewType type; @@ -80,9 +102,58 @@ class _PreviewState extends State { //Temporary image for now. Will have to change later final String imgPath = "assets/images/38582.jpg"; + // User's current location will fall back to _center when current location + // cannot be found + GeoPoint? currentLocation; + GeoPoint? targetLocation; + + late StreamSubscription positionStream; + + @override + void initState() { + startPositionStream(); + super.initState(); + } + + @override + void dispose() { + positionStream.cancel(); + super.dispose(); + } + + /** + * Starts the user's current location streaming upon state initialization + * Sets the camera to center on user's location by default + */ + void startPositionStream() async { + GeoPoint.current().then( + (location) { + currentLocation = location; + }, + ); + + positionStream = Geolocator.getPositionStream( + locationSettings: GeoPoint.getLocationSettings()) + .listen((Position? newPos) { + // prints user coordinates - useful for debugging + // print(newPos == null + // ? 'Unknown' + // : '${newPos.latitude.toString()}, ${newPos.longitude.toString()}'); + + if (newPos != null) + currentLocation = + GeoPoint(newPos.latitude, newPos.longitude, newPos.heading); + + setState(() {}); + }); + } + _PreviewState( this.challengeName, + this.challengeLat, + this.challengeLong, this.description, + this.imgUrl, this.difficulty, this.points, this.type, @@ -95,13 +166,31 @@ class _PreviewState extends State { ); @override Widget build(BuildContext context) { + if (challengeLat == null || challengeLong == null) { + return SizedBox( + height: MediaQuery.of(context).size.height * 0.75, + child: ClipRRect( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(10.0), + topRight: Radius.circular(10.0), + ), + //Overall Container + child: Container( + height: MediaQuery.of(context).size.height * 0.75, + width: MediaQuery.of(context).size.width, + color: Colors.white, + child: Text("Error: Cannot find challenge lat and long")), + ), + ); + } + //The popup box return SizedBox( height: MediaQuery.of(context).size.height * 0.75, child: ClipRRect( borderRadius: BorderRadius.only( - topLeft: Radius.circular(20.0), - topRight: Radius.circular(20.0), + topLeft: Radius.circular(10.0), + topRight: Radius.circular(10.0), ), //Overall Container @@ -112,49 +201,20 @@ class _PreviewState extends State { child: Column( children: [ //Image - Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.only( - topLeft: Radius.circular(3.0), - topRight: Radius.circular(3.0), - ), - image: DecorationImage( - image: AssetImage(imgPath), fit: BoxFit.cover)), - height: 150, - alignment: Alignment.topCenter, - //drag bar icon - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - child: SizedBox( - child: Align( - alignment: Alignment(1.1, -1.1), - child: Container( - width: 48, - height: 4, - alignment: Alignment.center, - decoration: BoxDecoration( - shape: BoxShape.rectangle, - color: Colors.white, - borderRadius: - BorderRadius.all(Radius.circular(20.0)), - ), - ), - ))) - ], - ), - ), + Image.network(imgUrl, + height: MediaQuery.of(context).size.height * 0.25, + width: double.infinity, + fit: BoxFit.cover), SizedBox(height: 20), // Row with starting location and distance Padding( padding: const EdgeInsets.symmetric( - horizontal: 25.0, vertical: 5), + horizontal: 20.0, vertical: 5), child: Align( alignment: Alignment.centerLeft, child: Row(children: [ - Icon(Icons.tour, + Icon(Icons.location_on, size: 24, color: Preview.purpleColor), Text(location, style: TextStyle( @@ -163,8 +223,18 @@ class _PreviewState extends State { Icon(Icons.directions_walk, size: 24, color: Preview.greyColor), Text( - "25" + // should call new parameter; replace later - "mi", + ' ' + + (currentLocation != null && + challengeLat != null && + challengeLong != null + ? (currentLocation!.distanceTo(GeoPoint( + challengeLat!, + challengeLong!, + 0)) / + 1609.34) + .toStringAsFixed(1) + : "?.?") + + " mi", style: TextStyle( fontSize: 20, color: Preview.greyColor)) ]), @@ -181,16 +251,20 @@ class _PreviewState extends State { color: Colors.black)), ), ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 25.0), - child: Align( - alignment: Alignment.centerLeft, - child: Text(description, - style: TextStyle( - // fontWeight: FontWeight.bold, - fontSize: 16, - fontFamily: 'Poppins', - color: Preview.greyColor)), + Container( + height: MediaQuery.of(context).size.height * 0.12, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 25.0, vertical: 8.0), + child: Align( + alignment: Alignment.topLeft, + child: Text(description, + style: TextStyle( + // fontWeight: FontWeight.bold, + fontSize: 16, + fontFamily: 'Poppins', + color: Preview.greyColor)), + ), ), ), Padding( @@ -228,7 +302,9 @@ class _PreviewState extends State { difficulty[0].toUpperCase() + difficulty.substring(1), style: TextStyle( - fontSize: 12, + fontSize: 16, + fontWeight: + FontWeight.w400, color: Colors.black), ), ), @@ -241,32 +317,18 @@ class _PreviewState extends State { child: Container( height: 36, alignment: Alignment.centerLeft, - child: Container( - child: Padding( - padding: const EdgeInsets.only( - left: 8, right: 8), - child: Align( - alignment: Alignment.center, - child: Text( - (points).toString() + "PTS", - style: TextStyle( - fontSize: 12, - color: Colors.white), - ), - ), + child: Row(children: [ + SvgPicture.asset( + "assets/icons/bearcoins.svg", + width: 40, + height: 40, ), - decoration: new BoxDecoration( - border: Border.all( - color: Color.fromARGB( - 255, 255, 199, 55), - ), - color: Color.fromARGB( - 255, 189, 135, 31), - borderRadius: - BorderRadius.circular(16), - shape: BoxShape.rectangle, - ), - ), + Text(points.toString() + " PTS", + style: TextStyle( + fontSize: 23, + fontWeight: FontWeight.w500, + color: Color(0xFFC17E19))) + ]), )), ], ), @@ -277,13 +339,12 @@ class _PreviewState extends State { ), (type == PreviewType.JOURNEY) ? Column(children: [ - SizedBox(height: 5), Padding( - padding: - const EdgeInsets.symmetric(horizontal: 25), + padding: const EdgeInsets.symmetric( + horizontal: 25, vertical: 5), child: Stack(children: [ Container( - width: 345, + width: MediaQuery.sizeOf(context).width * 0.9, height: 24, alignment: Alignment.centerLeft, child: Container( @@ -299,7 +360,8 @@ class _PreviewState extends State { width: (locationCount > 0 ? numberCompleted / locationCount : 0) * - 345, + MediaQuery.sizeOf(context).width * + 0.9, height: 24, alignment: Alignment.centerLeft, child: Container( @@ -312,7 +374,6 @@ class _PreviewState extends State { ), ), ])), - // SizedBox(height: 30), Padding( padding: const EdgeInsets.only( left: 25, right: 25, bottom: 15, top: 3), @@ -326,16 +387,18 @@ class _PreviewState extends State { MainAxisAlignment.spaceBetween, children: [ Spacer(), - Icon(Icons.location_on, - size: 15, - color: Preview.purpleColor), + SvgPicture.asset( + "assets/icons/pin.svg"), Text( - numberCompleted.toString() + + " " + + numberCompleted.toString() + "/" + locationCount.toString(), style: TextStyle( - fontSize: 15, - color: Colors.black)) + fontSize: 16, + fontWeight: FontWeight.w600, + color: Color.fromARGB( + 255, 110, 110, 110))) ]), ), ), @@ -348,7 +411,7 @@ class _PreviewState extends State { padding: EdgeInsets.symmetric(horizontal: 25), child: SizedBox( height: 50, - width: 345, + width: MediaQuery.sizeOf(context).width * 0.9, child: TextButton( style: ButtonStyle( backgroundColor: @@ -363,8 +426,8 @@ class _PreviewState extends State { .serverApi ?.setCurrentEvent( SetCurrentEventDto(eventId: eventId)); - print("setting current event to " + eventId); - Navigator.push( + Navigator.pop(context); + Navigator.pushReplacement( context, MaterialPageRoute( builder: (context) => GameplayPage())); diff --git a/game/lib/profile/achievement_cell.dart b/game/lib/profile/achievement_cell.dart index 8dd070f3..3749f720 100644 --- a/game/lib/profile/achievement_cell.dart +++ b/game/lib/profile/achievement_cell.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:flutter_svg/flutter_svg.dart'; +@Deprecated('achievements/achievement_cell.dart') /** * Widget that represents each individual achievement * @param name: Name of the achievement @@ -92,10 +92,6 @@ Widget achievementCell( ), ), ]), - Padding( - padding: const EdgeInsets.only(left: 8.0), - child: SvgPicture.asset("assets/icons/location.svg"), - ), Padding( padding: const EdgeInsets.only(left: 8.0), child: Text( diff --git a/game/lib/profile/completed_cell.dart b/game/lib/profile/completed_cell.dart index a7ccd817..194ec6ed 100644 --- a/game/lib/profile/completed_cell.dart +++ b/game/lib/profile/completed_cell.dart @@ -32,9 +32,9 @@ Widget completedCell(String name, String picture, String type, String date, child: ClipRRect( borderRadius: BorderRadius.circular(20), child: Image( - width: 80, - height: 80, - image: AssetImage(picture), + width: 64, + height: 64, + image: NetworkImage(picture), fit: BoxFit.cover, )), ), @@ -64,73 +64,20 @@ Widget completedCell(String name, String picture, String type, String date, ], ), Padding( - padding: const EdgeInsets.only(top: 8.0), + padding: const EdgeInsets.only(top: 5.0), child: Container( width: 200, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Container( - width: 80, - height: 25, - decoration: BoxDecoration( - border: Border.all( - color: Colors.purple, // Set the outline color - width: 2.0, // Set the outline width - ), - borderRadius: BorderRadius.circular( - 15.0), // Set border radius - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - SvgPicture.asset( - 'assets/icons/flag.svg', - ), - ], - ), - ), - Container( - width: 50, - height: 25, - decoration: BoxDecoration( - color: Color.fromRGBO(249, 236, 217, 1), - borderRadius: BorderRadius.circular( - 20.0), // Set border radius - ), - child: Center( - child: Text( - difficulty, - style: TextStyle( - fontSize: 10, - ), - ), - ), - ), - Container( - width: 50, - height: 25, - decoration: BoxDecoration( - border: Border.all( - color: Color.fromRGBO( - 189, 135, 31, 1), // Set the outline color - width: 1.38, // Set the outline width - ), - color: Color.fromRGBO(255, 199, 55, 1), - borderRadius: BorderRadius.circular( - 15.0), // Set border radius - ), - child: Center( - child: Text( - points.toString() + " PTS", - style: TextStyle( - fontSize: 10, - ), - ), - ), - ) - ], - ), + child: Row(children: [ + SvgPicture.asset( + "assets/icons/bearcoins.svg", + width: 20, + ), + Text(' ' + points.toString() + " PTS", + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.w500, + color: Color(0xFFC17E19))) + ]), ), ), ], diff --git a/game/lib/profile/completed_challenge_cell.dart b/game/lib/profile/completed_challenge_cell.dart index 627393a3..47964d03 100644 --- a/game/lib/profile/completed_challenge_cell.dart +++ b/game/lib/profile/completed_challenge_cell.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:carousel_slider/carousel_slider.dart'; +import 'package:flutter_svg/flutter_svg.dart'; /** * Widget that represents each completed challenge @@ -45,20 +46,22 @@ class _CompletedChallengeFullState extends State { @override Widget build(BuildContext context) { return Padding( - padding: const EdgeInsets.all(8.0), + padding: const EdgeInsets.only( + left: 24.0, right: 24.0, top: 24.0, bottom: 5.0), child: Container( - width: 345, - height: 526, + width: MediaQuery.sizeOf(context).width * 0.9, + height: MediaQuery.sizeOf(context).height * 0.6, decoration: ShapeDecoration( color: Colors.white, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), ), child: Stack( + alignment: Alignment.topCenter, children: [ CarouselSlider( key: _carouselKey, options: CarouselOptions( - height: 526.0, + height: MediaQuery.sizeOf(context).height, aspectRatio: 16 / 9, viewportFraction: 1.0, initialPage: 0, @@ -81,90 +84,86 @@ class _CompletedChallengeFullState extends State { child: Image.network( picture, fit: BoxFit.cover, + width: 390, + height: 20, ), ); }, ); }).toList(), ), - Positioned( - top: 16, - left: 16, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + Padding( + padding: const EdgeInsets.all(16.0), + child: Row( children: [ - Row( - children: [ - Container( - width: 130, - height: 31.58, - padding: const EdgeInsets.symmetric( - horizontal: 8, vertical: 2), - decoration: ShapeDecoration( - color: Colors.white.withOpacity(0.5), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Image.asset( - locationVector, - alignment: Alignment.centerLeft, - ), - Padding( - padding: EdgeInsets.symmetric(horizontal: 5)), - Text( - widget.location, - style: TextStyle( - color: Color(0xFF835A7C), - fontSize: 16, - fontFamily: 'Poppins', - fontWeight: FontWeight.w400, - height: 0, - ), - ), - ], - ), + // location type label + Container( + height: 31.58, + padding: + const EdgeInsets.symmetric(horizontal: 8, vertical: 2), + decoration: ShapeDecoration( + color: Colors.white.withOpacity(0.7), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), ), - SizedBox(width: 130), - Container( - padding: - EdgeInsets.symmetric(vertical: 2, horizontal: 15), - decoration: BoxDecoration( - color: Colors.black.withOpacity(0.5), - borderRadius: BorderRadius.circular(8), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Image.asset( + locationVector, + alignment: Alignment.centerLeft, ), - child: Text( - '${_currentIndex + 1}/${widget.pictures.length}', + Padding(padding: EdgeInsets.symmetric(horizontal: 5)), + Text( + widget.location, style: TextStyle( - color: Colors.white, + color: Color(0xFF835A7C), fontSize: 16, fontFamily: 'Poppins', fontWeight: FontWeight.w400, + height: 0, ), ), + ], + ), + ), + Spacer(), + // carousel number tracker label thing + Container( + padding: EdgeInsets.symmetric(vertical: 2, horizontal: 15), + decoration: BoxDecoration( + color: Colors.black.withOpacity(0.5), + borderRadius: BorderRadius.circular(8), + ), + child: Text( + '${_currentIndex + 1}/${widget.pictures.length}', + style: TextStyle( + color: Colors.white, + fontSize: 16, + fontFamily: 'Poppins', + fontWeight: FontWeight.w400, ), - ], - ) + ), + ), ], ), ), Positioned( bottom: 0, + left: 0, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( - width: 345, + width: 500, height: 122, color: Colors.white, child: Padding( padding: EdgeInsets.only(left: 16.0, top: 30.0), child: Container( - width: 242, + width: 350, height: 51, child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -224,7 +223,7 @@ class _CompletedChallengeFullState extends State { Row( children: [ Container( - width: 50, + width: 70, height: 29, padding: const EdgeInsets.symmetric( horizontal: 2, vertical: 4), @@ -234,57 +233,30 @@ class _CompletedChallengeFullState extends State { borderRadius: BorderRadius.circular(20), ), ), - child: Row( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: - CrossAxisAlignment.center, - children: [ - Text( - widget.difficulty, - style: TextStyle( - fontSize: 14, - fontFamily: 'Poppins', - fontWeight: FontWeight.w400, - height: 0, - ), + child: Center( + child: Text( + widget.difficulty, + style: TextStyle( + fontSize: 14, + fontFamily: 'Poppins', + fontWeight: FontWeight.w400, + height: 0, ), - ], + ), ), ), SizedBox(width: 8), - Container( - width: 70, - height: 29, - padding: const EdgeInsets.symmetric( - horizontal: 8, vertical: 0), - clipBehavior: Clip.antiAlias, - decoration: ShapeDecoration( - color: Color(0xFFC17E19), - shape: RoundedRectangleBorder( - side: BorderSide( - width: 3, color: Color(0xFFFFC737)), - borderRadius: BorderRadius.circular(20), - ), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: - CrossAxisAlignment.center, - children: [ - Text( - widget.points.toString() + 'PTS', - style: TextStyle( - fontSize: 14, - fontFamily: 'Poppins', - fontWeight: FontWeight.w400, - height: 0, - color: Colors.white), - ), - ], + Row(children: [ + SvgPicture.asset( + "assets/icons/bearcoins.svg", + width: 25, ), - ), + Text(' ' + widget.points.toString() + " PTS", + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: Color(0xFFC17E19))) + ]), ], ), ], diff --git a/game/lib/profile/completed_challenges_page.dart b/game/lib/profile/completed_challenges_page.dart index f149d7e2..63d460e7 100644 --- a/game/lib/profile/completed_challenges_page.dart +++ b/game/lib/profile/completed_challenges_page.dart @@ -4,7 +4,6 @@ import 'package:game/model/challenge_model.dart'; import 'package:game/model/event_model.dart'; import 'package:game/model/tracker_model.dart'; import 'package:game/model/user_model.dart'; -import 'package:game/profile/profile_page.dart'; import 'package:game/profile/completed_challenge_cell.dart'; import 'package:game/utils/utility_functions.dart'; import 'package:intl/intl.dart'; @@ -47,8 +46,6 @@ class CompletedChallengesPage extends StatelessWidget { child: CircularProgressIndicator(), ); } - var username = userModel.userData?.username; - var score = userModel.userData?.score; List> completedEvents = []; @@ -77,7 +74,7 @@ class CompletedChallengesPage extends StatelessWidget { //Sort so that the most recent events are first completedEvents.sort((a, b) => b.item1.compareTo(a.item1)); final itemCount = completedEvents.length; - return ListView.separated( + return ListView.builder( itemBuilder: (context, index) { var event = completedEvents[index].item2; var date = completedEvents[index].item1; @@ -86,22 +83,18 @@ class CompletedChallengesPage extends StatelessWidget { var pictureList = []; var locationList = []; - for (var challengeId in event.challenges ?? []) { - var challenge = challengeModel.getChallengeById(challengeId); - if (challenge != null) { - pictureList.add(challenge.imageUrl!); - locationList.add(challenge.location); - } - } - - //Calculate totalPoints. var totalPoints = 0; for (var challengeId in event.challenges ?? []) { var challenge = challengeModel.getChallengeById(challengeId); if (challenge != null) { + pictureList.add(challenge.imageUrl ?? + "https://upload.wikimedia.org/wikipedia/commons/b/b1/Missing-image-232x150.png"); + locationList + .add(friendlyLocation[challenge.location ?? 'ANY']); totalPoints += challenge.points ?? 0; } } + return CompletedChallengeFull( name: event.name!, pictures: pictureList, @@ -112,7 +105,6 @@ class CompletedChallengesPage extends StatelessWidget { points: totalPoints, ); }, - separatorBuilder: (context, index) => const Divider(), itemCount: itemCount); })); } diff --git a/game/lib/profile/edit_profile.dart b/game/lib/profile/edit_profile.dart new file mode 100644 index 00000000..3ba6b1ba --- /dev/null +++ b/game/lib/profile/edit_profile.dart @@ -0,0 +1,321 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:game/details_page/dropdown_widget.dart'; +import 'package:google_sign_in/google_sign_in.dart'; +import 'package:game/progress_indicators/circular_progress_indicator.dart'; + +import 'package:game/model/user_model.dart'; +import 'package:provider/provider.dart'; + +class EditProfileWidget extends StatefulWidget { + EditProfileWidget({ + Key? key, + }) : super(key: key); + + @override + _EditProfileState createState() => _EditProfileState(); +} + +class _EditProfileState extends State { + GoogleSignInAccount? user = null; + @override + void initState() { + super.initState(); + } + + final majorDropdownKey = ValueNotifier(0); + final updateButtonKey = ValueNotifier(0); + + String? newCollege; + String? newMajor; + String? newYear; + String? newUsername; + + var headingStyle = TextStyle( + color: Colors.black.withOpacity(0.8), + fontSize: 18, + fontFamily: 'Poppins', + fontWeight: FontWeight.w600, + height: 0, + ); + var buttonStyle = TextStyle( + color: Colors.white, + fontSize: 16, + fontFamily: 'Poppins', + fontWeight: FontWeight.w600, + height: 0, + ); + var fieldDecoration = InputDecoration( + contentPadding: + EdgeInsets.only(left: 20.0, right: 20.0, top: 10, bottom: 10), + labelStyle: TextStyle( + color: Color.fromARGB(51, 0, 0, 0), fontWeight: FontWeight.w400), + enabledBorder: OutlineInputBorder( + borderSide: BorderSide(color: Color.fromARGB(51, 0, 0, 0), width: 1.5), + borderRadius: BorderRadius.all(Radius.circular(10.0))), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.all(Radius.circular(10.0)), + borderSide: BorderSide( + color: Color.fromARGB(255, 255, 170, 91), + width: 1.5, + ), + ), + errorBorder: OutlineInputBorder( + borderRadius: BorderRadius.all(Radius.circular(10.0)), + borderSide: BorderSide( + color: Color.fromARGB(153, 233, 87, 85), + width: 1.5, + ), + ), + focusedErrorBorder: OutlineInputBorder( + borderRadius: BorderRadius.all(Radius.circular(10.0)), + borderSide: BorderSide( + color: Color.fromARGB(153, 233, 87, 85), + width: 1.5, + ), + ), + fillColor: Colors.white, + filled: true, + ); + + List _colleges = [ + "Agriculture and Life Sciences", + "Architecture, Art and Planning", + "Arts and Sciences", + "Business", + // "Computing and Information Science", + "Engineering", + "Human Ecology", + "Industrial and Labor Relations (ILR)", + "Public Policy", + "Cornell Tech", + "Law School", + // "Veterinary Medicine", + // "Weill Cornell Medicine" + ]; + Map> _majors = { + "Agriculture and Life Sciences": [], + "Architecture, Art and Planning": [], + "Business": [], + "Engineering": [ + "Computer Science", + "Information Science", + "Chemical Engineering" + ], + "Arts and Sciences": [ + "Computer Science", + "Mathematics", + "Chemistry", + "Biology", + "Psychology" + ], + "Human Ecology": [], + "Industrial and Labor Relations (ILR)": [], + "Public Policy": [], + "Cornell Tech": [], + "Law School": [], + }; + List _years = ["2024", "2025", "2026", "2027"]; + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Color.fromARGB(255, 255, 248, 241), + appBar: AppBar( + toolbarHeight: 70, + backgroundColor: Color.fromARGB(255, 237, 86, 86), + // Set widget before appBar title + leading: IconButton( + icon: const Icon(Icons.navigate_before), + color: Colors.white, + onPressed: () { + Navigator.pop(context); + }, + ), + title: const Text( + 'Edit Profile', + style: TextStyle( + color: Colors.white, + fontFamily: 'Poppins', + fontWeight: FontWeight.bold), + ), + actions: [], + ), + body: Center( + child: Consumer(builder: (context, userModel, child) { + if (userModel.userData == null) { + return CircularIndicator(); + } + + String? currUsername = userModel.userData?.username; + String? currYear = userModel.userData?.year; + String? currCollege = userModel.userData?.college; + String? currMajor = userModel.userData?.major; + + newUsername = currUsername; + newYear = currYear; + if (newYear != null && newYear!.isEmpty) { + newYear = null; + } + newCollege = currCollege; + if (newCollege != null && newCollege!.isEmpty) { + newCollege = null; + } + newMajor = currMajor; + if (newMajor != null && newMajor!.isEmpty) { + newMajor = null; + } + + bool fieldsChanged() { + if (newUsername == null || + newCollege == null || + newYear == null || + newMajor == null) { + return false; + } + if (newUsername!.isEmpty) { + return false; + } + // return true if any of the fields are different + return (newUsername != currUsername || + newYear != currYear || + newMajor != currMajor || + newCollege != currCollege); + } + + return LayoutBuilder( + builder: (BuildContext context, BoxConstraints constraints) { + return SizedBox( + width: constraints.maxWidth * 0.85, + child: Column( + // crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Padding( + padding: EdgeInsets.only(top: 30), + child: SvgPicture.asset( + "assets/images/yellow_bear_prof.svg", + height: 115, + width: 115), + ), + Container( + padding: EdgeInsets.only(top: 30), + width: double.infinity, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Username *', style: headingStyle), + SizedBox(height: 5), + TextFormField( + decoration: fieldDecoration, + initialValue: newUsername, + onChanged: (value) => { + newUsername = value, + updateButtonKey.value++ + }, + ) + ], + )), + Container( + padding: EdgeInsets.only(top: 15), + width: double.infinity, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('College', style: headingStyle), + SizedBox(height: 5), + DropdownWidget(newCollege, _colleges, + notifyParent: (val) => { + newCollege = val, + majorDropdownKey.value++, + updateButtonKey.value++ + }) + ], + )), + Container( + padding: EdgeInsets.only(top: 15), + width: double.infinity, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Major', style: headingStyle), + SizedBox(height: 5), + // Changing key forces a rebuild of child widget + ValueListenableBuilder( + valueListenable: majorDropdownKey, + builder: (BuildContext context, + double keyValue, Widget? child) { + return DropdownWidget( + // assigning UniqueKey will rebuild widget upon state change + key: ValueKey(keyValue), + (_majors[newCollege] == null || + !_majors[newCollege]! + .contains(newMajor)) + ? null + : newMajor, + newCollege == null + ? null + : _majors[newCollege], + notifyParent: (val) => { + newMajor = val, + updateButtonKey.value++ + }); + }) + ], + )), + Container( + padding: EdgeInsets.only(top: 15), + width: double.infinity, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Graduation Year', style: headingStyle), + SizedBox(height: 5), + DropdownWidget(newYear, _years, + notifyParent: (val) => + {newYear = val, updateButtonKey.value++}) + ], + )), + SizedBox(height: 100), + ValueListenableBuilder( + valueListenable: updateButtonKey, + builder: (BuildContext context, double keyValue, + Widget? child) { + return TextButton( + key: ValueKey(keyValue), + onPressed: !fieldsChanged() + ? null + : () { + userModel.updateUserData( + userModel.userData?.id ?? "", + newUsername, + newCollege, + newMajor, + newYear); + setState(() {}); + }, + style: TextButton.styleFrom( + backgroundColor: Color(0xFFE95755), + disabledBackgroundColor: Color(0xFFB9B9B9), + padding: const EdgeInsets.symmetric( + horizontal: 138, vertical: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8)), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + 'Update', + style: buttonStyle, + ), + ], + ), + ); + }) + ])); + }); + }))); + } +} diff --git a/game/lib/profile/profile_page.dart b/game/lib/profile/profile_page.dart index 8caab125..13cb552c 100644 --- a/game/lib/profile/profile_page.dart +++ b/game/lib/profile/profile_page.dart @@ -1,11 +1,12 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; +import 'package:game/achievements/achievements_page.dart'; import 'package:game/api/game_client_dto.dart'; import 'package:game/model/challenge_model.dart'; import 'package:game/model/event_model.dart'; import 'package:game/model/tracker_model.dart'; import 'package:game/model/user_model.dart'; -import 'package:game/profile/achievement_cell.dart'; +import 'package:game/achievements/achievement_cell.dart'; import 'package:game/profile/completed_cell.dart'; import 'package:game/profile/settings_page.dart'; import 'package:game/utils/utility_functions.dart'; @@ -25,7 +26,6 @@ class ProfilePage extends StatefulWidget { } class _ProfilePageState extends State { - final locationImage = "assets/images/38582.jpg"; @override Widget build(BuildContext context) { return Scaffold( @@ -40,6 +40,7 @@ class _ProfilePageState extends State { ); } var username = userModel.userData?.username; + var isGuest = userModel.userData?.authType == UserAuthTypeDto.device; var score = userModel.userData?.score; List> completedEvents = []; @@ -119,7 +120,7 @@ class _ProfilePageState extends State { Navigator.push( context, MaterialPageRoute( - builder: (context) => SettingsPage())); + builder: (context) => SettingsPage(isGuest))); }), ), ), @@ -143,7 +144,8 @@ class _ProfilePageState extends State { TextButton( onPressed: () { // Handle button press, e.g., navigate to details page - print('View Details button pressed'); + Navigator.of(context).push(MaterialPageRoute( + builder: (context) => AchievementsPage())); }, child: Text( 'View Details →', @@ -157,13 +159,21 @@ class _ProfilePageState extends State { ), ), //To be replaced with real data - achievementCell("Complete three challenges on the Arts quad", 4, - 6, locationImage), - achievementCell( - "Complete three challenges on the Engineering quad", - 4, - 6, - locationImage), + Padding( + padding: EdgeInsets.only(left: 30, right: 30), + child: Column(children: [ + AchievementCell( + "Complete three challenges", + SvgPicture.asset("assets/icons/achievementsilver.svg"), + 4, + 6), + SizedBox(height: 10), + AchievementCell( + "Complete three challenges", + SvgPicture.asset("assets/icons/achievementsilver.svg"), + 4, + 6), + ])), //Completed Events Padding( padding: const EdgeInsets.only(left: 24, right: 24.0), @@ -210,13 +220,18 @@ class _ProfilePageState extends State { //Calculate totalPoints. var totalPoints = 0; + var locationImage = ""; for (var challengeId in event.challenges ?? []) { var challenge = challengeModel.getChallengeById(challengeId); + locationImage = challenge?.imageUrl ?? + "https://upload.wikimedia.org/wikipedia/commons/b/b1/Missing-image-232x150.png"; + if (challenge != null) { totalPoints += challenge.points ?? 0; } } + return completedCell( event.name!, locationImage, diff --git a/game/lib/profile/settings_page.dart b/game/lib/profile/settings_page.dart index 4ece1965..d124c56d 100644 --- a/game/lib/profile/settings_page.dart +++ b/game/lib/profile/settings_page.dart @@ -1,11 +1,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/svg.dart'; -import 'package:game/utils/utility_functions.dart'; +import 'package:game/profile/edit_profile.dart'; import 'package:game/main.dart'; -import 'package:game/splash_page/splash_page.dart'; class SettingsPage extends StatelessWidget { - const SettingsPage({super.key}); + final bool isGuest; + const SettingsPage(this.isGuest, {super.key}); @override Widget build(BuildContext context) { @@ -38,72 +38,47 @@ class SettingsPage extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.center, children: [ SizedBox(height: 20), - Container( - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.only( - topRight: Radius.circular(10), - topLeft: Radius.circular(10), - ), - ), - child: TextButton( - onPressed: () {}, - style: TextButton.styleFrom( - padding: EdgeInsets.only(left: 20.0), - alignment: Alignment.centerLeft, - fixedSize: Size(constraints.maxWidth, 60)), - child: Row( - children: [ - Padding( - padding: EdgeInsets.only(right: 20.0), - child: SvgPicture.asset( - 'assets/icons/head.svg', - ), - ), - Text( - 'Edit Profile', - textAlign: TextAlign.left, - style: TextStyle( - fontFamily: 'Poppins', - fontSize: 16, - color: Colors.black), - ) - ], + if (!isGuest) + Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.only( + topRight: Radius.circular(10), + topLeft: Radius.circular(10), + ), ), - ), - ), - Divider(height: 1), - Container( - decoration: BoxDecoration( - color: Colors.white, - ), - child: TextButton( - onPressed: () {}, - style: TextButton.styleFrom( - padding: EdgeInsets.only(left: 20.0), - alignment: Alignment.centerLeft, - fixedSize: Size(constraints.maxWidth, 60)), - child: Row( - children: [ - Padding( - padding: EdgeInsets.only(right: 20.0), - child: SvgPicture.asset( - 'assets/icons/bell.svg', + child: TextButton( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => EditProfileWidget())); + }, + style: TextButton.styleFrom( + padding: EdgeInsets.only(left: 20.0), + alignment: Alignment.centerLeft, + fixedSize: Size(constraints.maxWidth, 60)), + child: Row( + children: [ + Padding( + padding: EdgeInsets.only(right: 20.0), + child: SvgPicture.asset( + 'assets/icons/head.svg', + ), ), - ), - Text( - 'Notifications', - textAlign: TextAlign.left, - style: TextStyle( - fontFamily: 'Poppins', - fontSize: 16, - color: Colors.black), - ) - ], + Text( + 'Edit Profile', + textAlign: TextAlign.left, + style: TextStyle( + fontFamily: 'Poppins', + fontSize: 16, + color: Colors.black), + ) + ], + ), ), ), - ), - Divider(height: 1), + if (!isGuest) Divider(height: 1), Container( decoration: BoxDecoration( color: Colors.white, @@ -147,14 +122,8 @@ class SettingsPage extends StatelessWidget { ), ), child: TextButton( - onPressed: () async => { - await client.disconnect(), - Navigator.pop(context), - Navigator.pushReplacement( - context, - MaterialPageRoute( - builder: (context) => SplashPageWidget())), - displayToast("Signed out", Status.success) + onPressed: () async { + await client.disconnect(); }, style: TextButton.styleFrom( padding: EdgeInsets.only(left: 20.0), diff --git a/game/lib/register_page/register_page.dart b/game/lib/register_page/register_page.dart index cc80d5fe..94ce5fa6 100644 --- a/game/lib/register_page/register_page.dart +++ b/game/lib/register_page/register_page.dart @@ -1,11 +1,12 @@ import 'package:flutter/material.dart'; +import 'package:game/api/game_client_dto.dart'; import 'package:game/details_page/details_page.dart'; import 'package:google_sign_in/google_sign_in.dart'; import 'package:flutter_svg/flutter_svg.dart'; class RegisterPageWidget extends StatefulWidget { - GoogleSignInAccount? user = null; - String? idToken = null; + final GoogleSignInAccount? user; + final String? idToken; RegisterPageWidget( {Key? key, required GoogleSignInAccount? this.user, @@ -21,21 +22,13 @@ class _RegisterPageWidgetState extends State { @override void initState() { super.initState(); - _selectedOption = "Graduate Student"; } - final List identityOptions = [ - "Undergraduate Student", - "Graduate Student", - "Faculty/Staff", - "Alumni" - ]; - - Map map1 = { - "Undergraduate Student": "UNDERGRADUATE", - "Graduate Student": "GRADUATE", - "Faculty/Staff": "FACULTY", - "Alumni": "ALUMNI" + Map entryToEnrollmentType = { + "Undergraduate Student": LoginEnrollmentTypeDto.UNDERGRADUATE, + "Graduate Student": LoginEnrollmentTypeDto.GRADUATE, + "Faculty/Staff": LoginEnrollmentTypeDto.FACULTY, + "Alumni": LoginEnrollmentTypeDto.ALUMNI }; @override @@ -48,7 +41,11 @@ class _RegisterPageWidgetState extends State { crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start, children: [ - SvgPicture.asset("assets/icons/back.svg"), + TextButton( + onPressed: () { + Navigator.pop(context); + }, + child: SvgPicture.asset("assets/icons/back.svg")), SvgPicture.asset("assets/images/register_progress.svg"), SizedBox(height: 40.0), Text("Who are you?", @@ -59,7 +56,7 @@ class _RegisterPageWidgetState extends State { )), SizedBox(height: 20.0), Column( - children: identityOptions.map((entry) { + children: entryToEnrollmentType.keys.map((name) { return Padding( padding: const EdgeInsets.only(top: 20.0), child: TextButton( @@ -68,17 +65,17 @@ class _RegisterPageWidgetState extends State { RoundedRectangleBorder( side: BorderSide( width: 2.0, - color: entry == _selectedOption + color: name == _selectedOption ? Color.fromARGB(255, 255, 170, 91) : Color.fromARGB(255, 217, 217, 217)), borderRadius: BorderRadius.circular(10.0))), - backgroundColor: entry == _selectedOption + backgroundColor: name == _selectedOption ? MaterialStatePropertyAll( Color.fromARGB(102, 255, 170, 91)) : MaterialStatePropertyAll(Colors.white)), onPressed: () => { setState(() { - _selectedOption = entry; + _selectedOption = name; }) }, child: Container( @@ -86,7 +83,7 @@ class _RegisterPageWidgetState extends State { height: 50, child: Align( alignment: Alignment.center, - child: Text(entry, + child: Text(name, style: TextStyle( color: Colors.black, fontSize: 16, @@ -122,7 +119,7 @@ class _RegisterPageWidgetState extends State { Navigator.of(context).push( MaterialPageRoute( builder: (context) => DetailsPageWidget( - userType: map1[_selectedOption]!, + userType: entryToEnrollmentType[_selectedOption]!, user: widget.user, idToken: widget.idToken, ), diff --git a/game/lib/splash_page/splash_page.dart b/game/lib/splash_page/splash_page.dart index 66831d4d..f5b07e49 100644 --- a/game/lib/splash_page/splash_page.dart +++ b/game/lib/splash_page/splash_page.dart @@ -1,6 +1,7 @@ +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:game/api/game_client_dto.dart'; import 'package:google_sign_in/google_sign_in.dart'; -import 'package:game/main.dart'; import 'package:provider/provider.dart'; import 'package:game/api/game_api.dart'; import 'package:game/utils/utility_functions.dart'; @@ -13,6 +14,8 @@ class SplashPageWidget extends StatelessWidget { final scaffoldKey = GlobalKey(); @override Widget build(BuildContext context) { + final client = Provider.of(context); + return Scaffold( backgroundColor: Colors.white, body: Stack( @@ -30,6 +33,22 @@ class SplashPageWidget extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.end, children: [ + StreamBuilder( + stream: client.clientApi.connectedStream, + builder: (context, snapshot) { + if (client.serverApi != null) { + print("ServerApi != null"); + WidgetsBinding.instance.addPostFrameCallback((_) { + Navigator.pushReplacement( + context, + MaterialPageRoute( + builder: (context) => BottomNavBar())); + displayToast("Signed in!", Status.success); + }); + } + + return Container(); + }), Consumer( builder: (context, apiClient, child) { return TextButton( @@ -43,18 +62,35 @@ class SplashPageWidget extends StatelessWidget { borderRadius: BorderRadius.circular(10.0)))), onPressed: () async { final GoogleSignInAccount? account = - await apiClient.connectGoogle(); + await apiClient.signinGoogle(); + if (account == null) { - displayToast("An error occured while signing you up.", + displayToast( + "An error occured while signing you into Google!", Status.error); - } else { - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => RegisterPageWidget( - user: account, idToken: null), - ), - ); + return; } + + if (!account.email.contains("@cornell.edu")) { + displayToast( + "Only Cornell-affiliated users may use Google Sign-in!", + Status.error); + return; + } + + final gRelogResult = + await apiClient.connectGoogleNoRegister(account); + + if (gRelogResult != null) { + return; + } + + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => RegisterPageWidget( + user: account, idToken: null), + ), + ); }, child: Container( width: 255, @@ -95,25 +131,12 @@ class SplashPageWidget extends StatelessWidget { RoundedRectangleBorder( borderRadius: BorderRadius.circular(10.0)))), onPressed: () async { - final String? id = await getId(); - print("GOT ID"); - print(id); - final endpoint_string = API_URL + "/device-login"; - final connectionResult = await client.connect( - id!, Uri.parse(endpoint_string), "GUEST", "", ""); + final connectionResult = await client.connectDevice( + "", LoginEnrollmentTypeDto.GUEST, "", "", "", []); if (connectionResult == null) { displayToast("An error occurred while signing you up!", Status.error); - } else { - //Connect to home page here. - print("Connection result:"); - print(connectionResult.body); - displayToast("Signed in!", Status.success); - Navigator.pushReplacement( - context, - MaterialPageRoute( - builder: (context) => BottomNavBar())); } }, child: Container( diff --git a/game/lib/utils/utility_functions.dart b/game/lib/utils/utility_functions.dart index 38d0ad96..1395732f 100644 --- a/game/lib/utils/utility_functions.dart +++ b/game/lib/utils/utility_functions.dart @@ -135,13 +135,22 @@ final Map friendlyDifficulty = { EventDifficultyDto.Hard: "Hard", }; -final Map friendlyLocation = { - "ENG_QUAD": "Eng Quad", - "ARTS_QUAD": "Arts Quad", - "AG_QUAD": "Ag Quad", - "NORTH_CAMPUS": "North Campus", - "WEST_CAMPUS": "West Campus", - "COLLEGETOWN": "Collegetown", - "ITHACA_COMMONS": "Ithaca Commons", - "ANY": "Cornell", +final Map friendlyLocation = { + ChallengeLocationDto.ENG_QUAD: "Eng Quad", + ChallengeLocationDto.ARTS_QUAD: "Arts Quad", + ChallengeLocationDto.AG_QUAD: "Ag Quad", + ChallengeLocationDto.NORTH_CAMPUS: "North Campus", + ChallengeLocationDto.WEST_CAMPUS: "West Campus", + ChallengeLocationDto.COLLEGETOWN: "Collegetown", + ChallengeLocationDto.ITHACA_COMMONS: "Ithaca Commons", + ChallengeLocationDto.ANY: "Cornell", +}; + +final Map friendlyCategory = { + EventCategoryDto.CAFE: "Cafe", + EventCategoryDto.DININGHALL: "Dining Hall", + EventCategoryDto.DORM: "Dorm", + EventCategoryDto.FOOD: "Food", + EventCategoryDto.HISTORICAL: "Historical", + EventCategoryDto.NATURE: "Nature", }; diff --git a/game/lib/widget/game_widget.dart b/game/lib/widget/game_widget.dart index 6cac3b98..29d3cf99 100644 --- a/game/lib/widget/game_widget.dart +++ b/game/lib/widget/game_widget.dart @@ -81,7 +81,8 @@ class _GameWidgetState extends State { gameModel.challengeId = curChallenge.id; gameModel.description = curChallenge.description ?? ""; gameModel.name = curChallenge.name ?? ""; - gameModel.imageUrl = curChallenge.imageUrl ?? ""; + gameModel.imageUrl = curChallenge.imageUrl ?? + "https://upload.wikimedia.org/wikipedia/commons/b/b1/Missing-image-232x150.png"; } if (serviceStatus == ServiceStatus.disabled || diff --git a/game/lib/widget/leaderboard_cell.dart b/game/lib/widget/leaderboard_cell.dart index babc6e8a..e0a3976d 100644 --- a/game/lib/widget/leaderboard_cell.dart +++ b/game/lib/widget/leaderboard_cell.dart @@ -49,8 +49,8 @@ Widget leaderBoardCell( padding: const EdgeInsets.symmetric(horizontal: 13), child: ClipRRect( child: Container( - width: 304, - height: 68, + width: MediaQuery.sizeOf(context).width * 0.65, + height: MediaQuery.sizeOf(context).height * 0.075, child: Row( children: [ Row( diff --git a/game/lib/widget/podium_cell.dart b/game/lib/widget/podium_cell.dart index 314bc2af..21586400 100644 --- a/game/lib/widget/podium_cell.dart +++ b/game/lib/widget/podium_cell.dart @@ -7,7 +7,7 @@ import 'package:game/utils/utility_functions.dart'; * they are on the podium. * @param name: the name of the user */ -Widget podiumCell(context, String name) { +Widget podiumCell(context, String name, bool isUser) { var nameStyle = TextStyle( color: Color(0xFF1E1E1E), fontSize: 14, @@ -24,20 +24,22 @@ Widget podiumCell(context, String name) { width: 49.128, height: 49.128, decoration: BoxDecoration( - color: constructColorFromUserName(name), - borderRadius: BorderRadius.circular(49.128)), + color: constructColorFromUserName(name), + borderRadius: BorderRadius.circular(49.128), + border: isUser + ? Border.all(color: Color.fromRGBO(255, 170, 91, 1), width: 2.5) + : null, + ), ), SizedBox( width: 80, - height: 38, + height: 20, child: Text( textAlign: TextAlign.center, name, style: nameStyle, // Allowing wrapping of the names in the podium overflow: TextOverflow.ellipsis, - softWrap: true, - maxLines: 2, ), ) ])); diff --git a/game/pubspec.yaml b/game/pubspec.yaml index e88c2163..05eb5c38 100644 --- a/game/pubspec.yaml +++ b/game/pubspec.yaml @@ -59,17 +59,18 @@ dependencies: carousel_slider: ^4.0.0 tuple: ^2.0.2 -flutter_icons: - android: true - ios: true - remove_alpha_ios: true - image_path: "assets/images/main_icon.png" - dev_dependencies: flutter_launcher_icons: flutter_test: sdk: flutter +flutter_launcher_icons: + android: "launcher_icon" + ios: true + image_path: "assets/images/main_icon.png" + min_sdk_android: 21 # android min sdk min:16, default 21 + remove_alpha_ios: true + # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec diff --git a/launch.json b/launch.json new file mode 100644 index 00000000..b1bbc7a7 --- /dev/null +++ b/launch.json @@ -0,0 +1,5 @@ +{ + "configurations": [ + + ] +} diff --git a/package.json b/package.json index 887954da..d58b1c1c 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "tests:e2e": "node ./scripts/tests.js e2e", "setup": "node ./scripts/setup.js", "updateapi": "node ./scripts/updateapi.js", + "formatall": "node ./scripts/formatall.js", "compilescripts": "tsc scripts/updateapi.ts --target es2021 --moduleResolution nodenext --module nodenext" }, "devDependencies": { diff --git a/scripts/formatall.js b/scripts/formatall.js new file mode 100644 index 00000000..a4ec47b9 --- /dev/null +++ b/scripts/formatall.js @@ -0,0 +1,16 @@ +const { execSync } = require("child_process"); +const { chdir } = require("process"); + +async function main() { + console.log("Formatting server..."); + chdir("./server"); + execSync("npm run format"); + console.log("Formatting admin..."); + chdir("../admin"); + execSync("npm run format"); + console.log("Formatting game..."); + chdir("../game"); + execSync("dart format lib"); +} + +main(); diff --git a/scripts/updateapi-lib/dartgen.js b/scripts/updateapi-lib/dartgen.js index db1c4f37..2747ee6d 100644 --- a/scripts/updateapi-lib/dartgen.js +++ b/scripts/updateapi-lib/dartgen.js @@ -191,22 +191,22 @@ function getDartClientApiFile(apiDefs) { `; } dartCode += ` - final _reconnectedController = StreamController.broadcast(sync: true); - Stream get reconnectedStream => _reconnectedController.stream; + final _reconnectedController = StreamController.broadcast(sync: true); + Stream get reconnectedStream => _reconnectedController.stream; - final _reconnectingController = StreamController.broadcast(sync: true); - Stream get reconnectingStream => _reconnectingController.stream; + final _reconnectingController = StreamController.broadcast(sync: true); + Stream get reconnectingStream => _reconnectingController.stream; - final _connectedController = StreamController.broadcast(sync: true); - Stream get connectedStream => _connectedController.stream; + final _connectedController = StreamController.broadcast(sync: true); + Stream get connectedStream => _connectedController.stream; - final disconnectedController = StreamController.broadcast(sync: true); - Stream get disconnectedStream => disconnectedController.stream; + final disconnectedController = StreamController.broadcast(sync: true); + Stream get disconnectedStream => disconnectedController.stream; void connectSocket(Socket sock) { - sock.onReconnect((data) => _reconnectingController.add(null)); - sock.onReconnecting((data) => _reconnectedController.add(null)); - sock.onDisconnect((data) => disconnectedController.add(null)); + sock.onReconnect((data) => _reconnectedController.add(true)); + sock.onReconnecting((data) => _reconnectingController.add(true)); + sock.onDisconnect((data) => disconnectedController.add(true)); `; for (const [ev, dto] of apiDefs.clientEntrypoints.entries()) { @@ -219,7 +219,7 @@ function getDartClientApiFile(apiDefs) { `; } dartCode += ` - _connectedController.add(null); + _connectedController.add(true); } GameClientApi() {} @@ -268,7 +268,7 @@ function getDartServerApiFile(apiDefs) { void _invokeWithRefresh(String ev, Map data) { _refreshEv = ev; _refreshDat = data; - print(ev); + //print(ev); _socket.emit(ev, data); } `; diff --git a/scripts/updateapi-lib/dartgen.ts b/scripts/updateapi-lib/dartgen.ts index 35f16224..80cc036e 100644 --- a/scripts/updateapi-lib/dartgen.ts +++ b/scripts/updateapi-lib/dartgen.ts @@ -197,22 +197,22 @@ export function getDartClientApiFile(apiDefs: ApiDefs) { } dartCode += ` - final _reconnectedController = StreamController.broadcast(sync: true); - Stream get reconnectedStream => _reconnectedController.stream; + final _reconnectedController = StreamController.broadcast(sync: true); + Stream get reconnectedStream => _reconnectedController.stream; - final _reconnectingController = StreamController.broadcast(sync: true); - Stream get reconnectingStream => _reconnectingController.stream; + final _reconnectingController = StreamController.broadcast(sync: true); + Stream get reconnectingStream => _reconnectingController.stream; - final _connectedController = StreamController.broadcast(sync: true); - Stream get connectedStream => _connectedController.stream; + final _connectedController = StreamController.broadcast(sync: true); + Stream get connectedStream => _connectedController.stream; - final disconnectedController = StreamController.broadcast(sync: true); - Stream get disconnectedStream => disconnectedController.stream; + final disconnectedController = StreamController.broadcast(sync: true); + Stream get disconnectedStream => disconnectedController.stream; void connectSocket(Socket sock) { - sock.onReconnect((data) => _reconnectingController.add(null)); - sock.onReconnecting((data) => _reconnectedController.add(null)); - sock.onDisconnect((data) => disconnectedController.add(null)); + sock.onReconnect((data) => _reconnectedController.add(true)); + sock.onReconnecting((data) => _reconnectingController.add(true)); + sock.onDisconnect((data) => disconnectedController.add(true)); `; @@ -227,7 +227,7 @@ export function getDartClientApiFile(apiDefs: ApiDefs) { } dartCode += ` - _connectedController.add(null); + _connectedController.add(true); } GameClientApi() {} @@ -277,7 +277,7 @@ export function getDartServerApiFile(apiDefs: ApiDefs) { void _invokeWithRefresh(String ev, Map data) { _refreshEv = ev; _refreshDat = data; - print(ev); + //print(ev); _socket.emit(ev, data); } `; diff --git a/scripts/updateapi-lib/dtoscanner.js b/scripts/updateapi-lib/dtoscanner.js index a69d020f..fd4fbd7c 100644 --- a/scripts/updateapi-lib/dtoscanner.js +++ b/scripts/updateapi-lib/dtoscanner.js @@ -3,96 +3,106 @@ Object.defineProperty(exports, "__esModule", { value: true }); exports.getDtoDefinitions = void 0; const ts_morph_1 = require("ts-morph"); function getDtoDefinitions() { - const project = new ts_morph_1.Project({}); - project.addSourceFilesAtPaths("server/src/*/*.dto.ts"); - console.log(`Discovered ${project.getSourceFiles().length} DTO files!`); - console.log(); - const enumDtos = new Map(); - const baseDtos = new Map(); - for (const file of project.getSourceFiles()) { - const interfs = file.getInterfaces(); - const enums = file.getEnums(); - console.log(`${file.getBaseName()}: ${enums.length} enums, ${interfs.length} DTOs`); - for (const enum_ of enums) { - const vals = enum_.getMembers().map((val) => val.getName()); - enumDtos.set(enum_.getName(), vals); - } - for (const interf of interfs) { - const props = interf.getProperties(); - const baseDto = new Map(); - const interfName = interf.getName(); - baseDtos.set(interfName, baseDto); - for (const prop of props) { - const propType = prop.getType(); - const propName = prop.getName(); - const isOptional = prop.hasQuestionToken(); - if (propType.isString() || propType.getArrayElementType()?.isString()) { - // str - baseDto.set(propName, [ - "string", - propType.isArray() ? "PRIMITIVE[]" : "PRIMITIVE", - isOptional, - ]); - } - else if (propType.isNumber() || - propType.getArrayElementType()?.isNumber()) { - // num - baseDto.set(propName, [ - "number", - propType.isArray() ? "PRIMITIVE[]" : "PRIMITIVE", - isOptional, - ]); - } - else if (propType.isBoolean() || - propType.getArrayElementType()?.isBoolean()) { - // num - baseDto.set(propName, [ - "boolean", - propType.isArray() ? "PRIMITIVE[]" : "PRIMITIVE", - isOptional, - ]); - } - else if (propType.isInterface() || - propType.getArrayElementType()?.isInterface() || - propType.isEnum() || - propType.getArrayElementType()?.isEnum()) { - let name = propType.isArray() - ? propType.getArrayElementTypeOrThrow().getText() - : propType.getText(); - const isEnum = propType.isEnum() || propType.getArrayElementType()?.isEnum(); - if (name.includes(".")) { - name = name.split(".").pop(); - } - const fieldType = propType.isArray() - ? isEnum - ? "ENUM_DTO[]" - : "DEPENDENT_DTO[]" - : isEnum - ? "ENUM_DTO" - : "DEPENDENT_DTO"; - baseDto.set(propName, [name, fieldType, isOptional]); - } - else if (propType.isUnion() || - propType.getArrayElementType()?.isUnion()) { - // enum - const enumName = interfName.replace("Dto", "") + - propName[0].toUpperCase() + - propName.substring(1) + - "Dto"; - enumDtos.set(enumName, propType - .getUnionTypes() - .map((t) => t.getLiteralValue()?.toString() ?? "")); - baseDto.set(propName, [ - enumName, - propType.isArray() ? "ENUM_DTO[]" : "ENUM_DTO", - isOptional, - ]); - } - } + const project = new ts_morph_1.Project({}); + project.addSourceFilesAtPaths("server/src/*/*.dto.ts"); + console.log(`Discovered ${project.getSourceFiles().length} DTO files!`); + console.log(); + const enumDtos = new Map(); + const baseDtos = new Map(); + for (const file of project.getSourceFiles()) { + const interfs = file.getInterfaces(); + const enums = file.getEnums(); + console.log( + `${file.getBaseName()}: ${enums.length} enums, ${interfs.length} DTOs` + ); + for (const enum_ of enums) { + const vals = enum_.getMembers().map((val) => val.getName()); + enumDtos.set(enum_.getName(), vals); + } + for (const interf of interfs) { + const props = interf.getProperties(); + const baseDto = new Map(); + const interfName = interf.getName(); + baseDtos.set(interfName, baseDto); + for (const prop of props) { + const propType = prop.getType(); + const propName = prop.getName(); + const isOptional = prop.hasQuestionToken(); + if (propType.isString() || propType.getArrayElementType()?.isString()) { + // str + baseDto.set(propName, [ + "string", + propType.isArray() ? "PRIMITIVE[]" : "PRIMITIVE", + isOptional, + ]); + } else if ( + propType.isNumber() || + propType.getArrayElementType()?.isNumber() + ) { + // num + baseDto.set(propName, [ + "number", + propType.isArray() ? "PRIMITIVE[]" : "PRIMITIVE", + isOptional, + ]); + } else if ( + propType.isBoolean() || + propType.getArrayElementType()?.isBoolean() + ) { + // num + baseDto.set(propName, [ + "boolean", + propType.isArray() ? "PRIMITIVE[]" : "PRIMITIVE", + isOptional, + ]); + } else if ( + propType.isInterface() || + propType.getArrayElementType()?.isInterface() || + propType.isEnum() || + propType.getArrayElementType()?.isEnum() + ) { + let name = propType.isArray() + ? propType.getArrayElementTypeOrThrow().getText() + : propType.getText(); + const isEnum = + propType.isEnum() || propType.getArrayElementType()?.isEnum(); + if (name.includes(".")) { + name = name.split(".").pop(); + } + const fieldType = propType.isArray() + ? isEnum + ? "ENUM_DTO[]" + : "DEPENDENT_DTO[]" + : isEnum + ? "ENUM_DTO" + : "DEPENDENT_DTO"; + baseDto.set(propName, [name, fieldType, isOptional]); + } else if ( + propType.isUnion() || + propType.getArrayElementType()?.isUnion() + ) { + // enum + const enumName = + interfName.replace("Dto", "") + + propName[0].toUpperCase() + + propName.substring(1) + + "Dto"; + enumDtos.set( + enumName, + propType + .getUnionTypes() + .map((t) => t.getLiteralValue()?.toString() ?? "") + ); + baseDto.set(propName, [ + enumName, + propType.isArray() ? "ENUM_DTO[]" : "ENUM_DTO", + isOptional, + ]); } + } } - console.log(new Array(enumDtos.keys()), new Array(baseDtos.keys())); - console.log(); - return { enumDtos, baseDtos }; + } + console.log(); + return { enumDtos, baseDtos }; } exports.getDtoDefinitions = getDtoDefinitions; diff --git a/server/.env b/server/.env index 4bc29eed..9fe3118d 100644 --- a/server/.env +++ b/server/.env @@ -3,7 +3,6 @@ DATABASE_URL=postgresql://postgres:test@localhost:5432/postgres GOOGLE_IOS_CLIENT_ID=757523123677-cso74eimkjqkcuce69nrf4c637k3db6t.apps.googleusercontent.com GOOGLE_ANDROID_CLIENT_ID=757523123677-2nv6haiqnvklhb134cgg5qe8bia4du4q.apps.googleusercontent.com GOOGLE_WEB_CLIENT_ID=757523123677-2nv6haiqnvklhb134cgg5qe8bia4du4q.apps.googleusercontent.com - JWT_ACCESS_SECRET=example_access_secret JWT_REFRESH_SECRET=example_refresh_secret JWT_ACCESS_EXPIRATION=120m diff --git a/server/package.json b/server/package.json index d9505133..ef5b6150 100644 --- a/server/package.json +++ b/server/package.json @@ -13,7 +13,7 @@ "checkformat": "prettier --check \"src/**/*.ts\" \"test/**/*.ts\"", "start": "nest start", "start:dev": "nest start --watch", - "start:debug": "nest start --debug --watch", + "start:debug": "nest start --debug 0.0.0.0:9229 --watch", "start:prod": "node dist/main", "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", "test": "jest", diff --git a/server/prisma/migrations/20221024031934_initial/migration.sql b/server/prisma/migrations/20221024031934_initial/migration.sql deleted file mode 100644 index 1aafe033..00000000 --- a/server/prisma/migrations/20221024031934_initial/migration.sql +++ /dev/null @@ -1,200 +0,0 @@ --- CreateEnum -CREATE TYPE "AuthType" AS ENUM ('GOOGLE', 'APPLE', 'DEVICE', 'NONE'); - --- CreateEnum -CREATE TYPE "EventRewardType" AS ENUM ('LIMITED_TIME', 'PERPETUAL'); - --- CreateTable -CREATE TABLE "User" ( - "id" TEXT NOT NULL, - "authToken" TEXT NOT NULL, - "authType" "AuthType" NOT NULL, - "username" TEXT NOT NULL, - "email" TEXT NOT NULL, - "hashedRefreshToken" TEXT NOT NULL, - "superuser" BOOLEAN NOT NULL, - "adminGranted" BOOLEAN NOT NULL, - "adminRequested" BOOLEAN NOT NULL, - "score" INTEGER NOT NULL, - "groupId" TEXT NOT NULL, - "restrictedById" TEXT, - "generatedById" TEXT, - "isRanked" BOOLEAN NOT NULL DEFAULT true, - - CONSTRAINT "User_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "Group" ( - "id" TEXT NOT NULL, - "friendlyId" TEXT NOT NULL, - "hostId" TEXT, - "curEventId" TEXT NOT NULL, - - CONSTRAINT "Group_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "Challenge" ( - "id" TEXT NOT NULL, - "linkedEventId" TEXT NOT NULL, - "eventIndex" INTEGER NOT NULL, - "name" TEXT NOT NULL, - "description" TEXT NOT NULL, - "imageUrl" TEXT NOT NULL, - "latitude" DOUBLE PRECISION NOT NULL, - "longitude" DOUBLE PRECISION NOT NULL, - "awardingRadius" DOUBLE PRECISION NOT NULL, - "closeRadius" DOUBLE PRECISION NOT NULL, - - CONSTRAINT "Challenge_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "EventBase" ( - "id" TEXT NOT NULL, - "requiredMembers" INTEGER NOT NULL, - "skippingEnabled" BOOLEAN NOT NULL, - "isDefault" BOOLEAN NOT NULL, - "name" TEXT NOT NULL, - "description" TEXT NOT NULL, - "rewardType" "EventRewardType" NOT NULL, - "indexable" BOOLEAN NOT NULL, - "endTime" TIMESTAMP(3) NOT NULL, - "minimumScore" INTEGER NOT NULL DEFAULT 1, - - CONSTRAINT "EventBase_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "EventReward" ( - "id" TEXT NOT NULL, - "userId" TEXT, - "eventId" TEXT NOT NULL, - "description" TEXT NOT NULL, - "redeemInfo" TEXT NOT NULL, - "isRedeemed" BOOLEAN NOT NULL, - - CONSTRAINT "EventReward_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "EventTracker" ( - "id" TEXT NOT NULL, - "userId" TEXT NOT NULL, - "score" INTEGER NOT NULL, - "isRankedForEvent" BOOLEAN NOT NULL DEFAULT true, - "cooldownEnd" TIMESTAMP(3) NOT NULL, - "eventId" TEXT NOT NULL, - "curChallengeId" TEXT NOT NULL, - - CONSTRAINT "EventTracker_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "PrevChallenge" ( - "id" SERIAL NOT NULL, - "userId" TEXT NOT NULL, - "challengeId" TEXT NOT NULL, - "trackerId" TEXT NOT NULL, - "timestamp" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - - CONSTRAINT "PrevChallenge_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "Organization" ( - "id" TEXT NOT NULL, - "displayName" TEXT NOT NULL, - "name" TEXT NOT NULL, - "canEditUsername" BOOLEAN NOT NULL, - - CONSTRAINT "Organization_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "_EventBaseToOrganization" ( - "A" TEXT NOT NULL, - "B" TEXT NOT NULL -); - --- CreateTable -CREATE TABLE "_participant" ( - "A" INTEGER NOT NULL, - "B" TEXT NOT NULL -); - --- CreateIndex -CREATE UNIQUE INDEX "Group_friendlyId_key" ON "Group"("friendlyId"); - --- CreateIndex -CREATE UNIQUE INDEX "Group_hostId_key" ON "Group"("hostId"); - --- CreateIndex -CREATE UNIQUE INDEX "EventBase_isDefault_key" ON "EventBase"("isDefault"); - --- CreateIndex -CREATE UNIQUE INDEX "_EventBaseToOrganization_AB_unique" ON "_EventBaseToOrganization"("A", "B"); - --- CreateIndex -CREATE INDEX "_EventBaseToOrganization_B_index" ON "_EventBaseToOrganization"("B"); - --- CreateIndex -CREATE UNIQUE INDEX "_participant_AB_unique" ON "_participant"("A", "B"); - --- CreateIndex -CREATE INDEX "_participant_B_index" ON "_participant"("B"); - --- AddForeignKey -ALTER TABLE "User" ADD CONSTRAINT "User_groupId_fkey" FOREIGN KEY ("groupId") REFERENCES "Group"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "User" ADD CONSTRAINT "User_restrictedById_fkey" FOREIGN KEY ("restrictedById") REFERENCES "Organization"("id") ON DELETE SET NULL ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "User" ADD CONSTRAINT "User_generatedById_fkey" FOREIGN KEY ("generatedById") REFERENCES "Organization"("id") ON DELETE SET NULL ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "Group" ADD CONSTRAINT "Group_hostId_fkey" FOREIGN KEY ("hostId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "Group" ADD CONSTRAINT "Group_curEventId_fkey" FOREIGN KEY ("curEventId") REFERENCES "EventBase"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "Challenge" ADD CONSTRAINT "Challenge_linkedEventId_fkey" FOREIGN KEY ("linkedEventId") REFERENCES "EventBase"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "EventReward" ADD CONSTRAINT "EventReward_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "EventReward" ADD CONSTRAINT "EventReward_eventId_fkey" FOREIGN KEY ("eventId") REFERENCES "EventBase"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "EventTracker" ADD CONSTRAINT "EventTracker_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "EventTracker" ADD CONSTRAINT "EventTracker_eventId_fkey" FOREIGN KEY ("eventId") REFERENCES "EventBase"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "EventTracker" ADD CONSTRAINT "EventTracker_curChallengeId_fkey" FOREIGN KEY ("curChallengeId") REFERENCES "Challenge"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "PrevChallenge" ADD CONSTRAINT "PrevChallenge_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "PrevChallenge" ADD CONSTRAINT "PrevChallenge_challengeId_fkey" FOREIGN KEY ("challengeId") REFERENCES "Challenge"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "PrevChallenge" ADD CONSTRAINT "PrevChallenge_trackerId_fkey" FOREIGN KEY ("trackerId") REFERENCES "EventTracker"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "_EventBaseToOrganization" ADD CONSTRAINT "_EventBaseToOrganization_A_fkey" FOREIGN KEY ("A") REFERENCES "EventBase"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "_EventBaseToOrganization" ADD CONSTRAINT "_EventBaseToOrganization_B_fkey" FOREIGN KEY ("B") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "_participant" ADD CONSTRAINT "_participant_A_fkey" FOREIGN KEY ("A") REFERENCES "PrevChallenge"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "_participant" ADD CONSTRAINT "_participant_B_fkey" FOREIGN KEY ("B") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/server/prisma/migrations/20221129233927_reward_order/migration.sql b/server/prisma/migrations/20221129233927_reward_order/migration.sql deleted file mode 100644 index e13bd437..00000000 --- a/server/prisma/migrations/20221129233927_reward_order/migration.sql +++ /dev/null @@ -1,11 +0,0 @@ -/* - Warnings: - - - Added the required column `eventIndex` to the `EventReward` table without a default value. This is not possible if the table is not empty. - -*/ --- DropIndex -DROP INDEX "EventBase_isDefault_key"; - --- AlterTable -ALTER TABLE "EventReward" ADD COLUMN "eventIndex" INTEGER NOT NULL; diff --git a/server/prisma/migrations/20221226061809_move_to_orgs/migration.sql b/server/prisma/migrations/20221226061809_move_to_orgs/migration.sql deleted file mode 100644 index a619f9f5..00000000 --- a/server/prisma/migrations/20221226061809_move_to_orgs/migration.sql +++ /dev/null @@ -1,125 +0,0 @@ -/* - Warnings: - - - You are about to drop the column `isDefault` on the `EventBase` table. All the data in the column will be lost. - - You are about to drop the column `skippingEnabled` on the `EventBase` table. All the data in the column will be lost. - - You are about to drop the column `cooldownEnd` on the `EventTracker` table. All the data in the column will be lost. - - You are about to drop the column `canEditUsername` on the `Organization` table. All the data in the column will be lost. - - You are about to drop the column `displayName` on the `Organization` table. All the data in the column will be lost. - - You are about to drop the column `adminGranted` on the `User` table. All the data in the column will be lost. - - You are about to drop the column `adminRequested` on the `User` table. All the data in the column will be lost. - - You are about to drop the column `generatedById` on the `User` table. All the data in the column will be lost. - - You are about to drop the column `restrictedById` on the `User` table. All the data in the column will be lost. - - You are about to drop the column `superuser` on the `User` table. All the data in the column will be lost. - - You are about to drop the `_EventBaseToOrganization` table. If the table is not empty, all the data it contains will be lost. - - A unique constraint covering the columns `[defaultChallengeId]` on the table `EventBase` will be added. If there are existing duplicate values, this will fail. - - Added the required column `defaultChallengeId` to the `EventBase` table without a default value. This is not possible if the table is not empty. - - Added the required column `accessCode` to the `Organization` table without a default value. This is not possible if the table is not empty. - - Added the required column `defaultEventId` to the `Organization` table without a default value. This is not possible if the table is not empty. - - Added the required column `specialUsage` to the `Organization` table without a default value. This is not possible if the table is not empty. - - Added the required column `administrator` to the `User` table without a default value. This is not possible if the table is not empty. - -*/ --- CreateEnum -CREATE TYPE "OrganizationSpecialUsage" AS ENUM ('DEVICE_LOGIN', 'CORNELL_LOGIN', 'NONE'); - --- DropForeignKey -ALTER TABLE "User" DROP CONSTRAINT "User_generatedById_fkey"; - --- DropForeignKey -ALTER TABLE "User" DROP CONSTRAINT "User_restrictedById_fkey"; - --- DropForeignKey -ALTER TABLE "_EventBaseToOrganization" DROP CONSTRAINT "_EventBaseToOrganization_A_fkey"; - --- DropForeignKey -ALTER TABLE "_EventBaseToOrganization" DROP CONSTRAINT "_EventBaseToOrganization_B_fkey"; - --- AlterTable -ALTER TABLE "EventBase" DROP COLUMN "isDefault", -DROP COLUMN "skippingEnabled", -ADD COLUMN "defaultChallengeId" TEXT NOT NULL; - --- AlterTable -ALTER TABLE "EventTracker" DROP COLUMN "cooldownEnd"; - --- AlterTable -ALTER TABLE "Organization" DROP COLUMN "canEditUsername", -DROP COLUMN "displayName", -ADD COLUMN "accessCode" TEXT NOT NULL, -ADD COLUMN "defaultEventId" TEXT NOT NULL, -ADD COLUMN "specialUsage" "OrganizationSpecialUsage" NOT NULL; - --- AlterTable -ALTER TABLE "User" DROP COLUMN "adminGranted", -DROP COLUMN "adminRequested", -DROP COLUMN "generatedById", -DROP COLUMN "restrictedById", -DROP COLUMN "superuser", -ADD COLUMN "administrator" BOOLEAN NOT NULL; - --- DropTable -DROP TABLE "_EventBaseToOrganization"; - --- CreateTable -CREATE TABLE "_events" ( - "A" TEXT NOT NULL, - "B" TEXT NOT NULL -); - --- CreateTable -CREATE TABLE "_player" ( - "A" TEXT NOT NULL, - "B" TEXT NOT NULL -); - --- CreateTable -CREATE TABLE "_manager" ( - "A" TEXT NOT NULL, - "B" TEXT NOT NULL -); - --- CreateIndex -CREATE UNIQUE INDEX "_events_AB_unique" ON "_events"("A", "B"); - --- CreateIndex -CREATE INDEX "_events_B_index" ON "_events"("B"); - --- CreateIndex -CREATE UNIQUE INDEX "_player_AB_unique" ON "_player"("A", "B"); - --- CreateIndex -CREATE INDEX "_player_B_index" ON "_player"("B"); - --- CreateIndex -CREATE UNIQUE INDEX "_manager_AB_unique" ON "_manager"("A", "B"); - --- CreateIndex -CREATE INDEX "_manager_B_index" ON "_manager"("B"); - --- CreateIndex -CREATE UNIQUE INDEX "EventBase_defaultChallengeId_key" ON "EventBase"("defaultChallengeId"); - --- AddForeignKey -ALTER TABLE "EventBase" ADD CONSTRAINT "EventBase_defaultChallengeId_fkey" FOREIGN KEY ("defaultChallengeId") REFERENCES "Challenge"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "Organization" ADD CONSTRAINT "Organization_defaultEventId_fkey" FOREIGN KEY ("defaultEventId") REFERENCES "EventBase"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "_events" ADD CONSTRAINT "_events_A_fkey" FOREIGN KEY ("A") REFERENCES "EventBase"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "_events" ADD CONSTRAINT "_events_B_fkey" FOREIGN KEY ("B") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "_player" ADD CONSTRAINT "_player_A_fkey" FOREIGN KEY ("A") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "_player" ADD CONSTRAINT "_player_B_fkey" FOREIGN KEY ("B") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "_manager" ADD CONSTRAINT "_manager_A_fkey" FOREIGN KEY ("A") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "_manager" ADD CONSTRAINT "_manager_B_fkey" FOREIGN KEY ("B") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/server/prisma/migrations/20221228035822_make_linked_nullable/migration.sql b/server/prisma/migrations/20221228035822_make_linked_nullable/migration.sql deleted file mode 100644 index 1d052cf4..00000000 --- a/server/prisma/migrations/20221228035822_make_linked_nullable/migration.sql +++ /dev/null @@ -1,2 +0,0 @@ --- AlterTable -ALTER TABLE "Challenge" ALTER COLUMN "linkedEventId" DROP NOT NULL; diff --git a/server/prisma/migrations/20240302230420_feb3rd2024_migration/migration.sql b/server/prisma/migrations/20240302230420_feb3rd2024_migration/migration.sql deleted file mode 100644 index 6a9741a8..00000000 --- a/server/prisma/migrations/20240302230420_feb3rd2024_migration/migration.sql +++ /dev/null @@ -1,249 +0,0 @@ -/* - Warnings: - - - You are about to drop the column `defaultChallengeId` on the `EventBase` table. All the data in the column will be lost. - - You are about to drop the column `minimumScore` on the `EventBase` table. All the data in the column will be lost. - - You are about to drop the column `rewardType` on the `EventBase` table. All the data in the column will be lost. - - You are about to drop the column `defaultEventId` on the `Organization` table. All the data in the column will be lost. - - You are about to drop the `EventReward` table. If the table is not empty, all the data it contains will be lost. - - You are about to drop the `_events` table. If the table is not empty, all the data it contains will be lost. - - You are about to drop the `_manager` table. If the table is not empty, all the data it contains will be lost. - - You are about to drop the `_participant` table. If the table is not empty, all the data it contains will be lost. - - You are about to drop the `_player` table. If the table is not empty, all the data it contains will be lost. - - Added the required column `difficulty` to the `EventBase` table without a default value. This is not possible if the table is not empty. - - Added the required column `latitude` to the `EventBase` table without a default value. This is not possible if the table is not empty. - - Added the required column `longitude` to the `EventBase` table without a default value. This is not possible if the table is not empty. - - Added the required column `timeLimitation` to the `EventBase` table without a default value. This is not possible if the table is not empty. - - Added the required column `enrollmentType` to the `User` table without a default value. This is not possible if the table is not empty. - - Added the required column `year` to the `User` table without a default value. This is not possible if the table is not empty. - -*/ --- CreateEnum -CREATE TYPE "TimeLimitationType" AS ENUM ('LIMITED_TIME', 'PERPETUAL'); - --- CreateEnum -CREATE TYPE "EnrollmentType" AS ENUM ('UNDERGRADUATE', 'GRADUATE', 'FACULTY', 'ALUMNI'); - --- CreateEnum -CREATE TYPE "LocationType" AS ENUM ('ENG_QUAD', 'ARTS_QUAD', 'AG_QUAD', 'NORTH_CAMPUS', 'WEST_CAMPUS', 'COLLEGETOWN', 'ITHACA_COMMONS', 'ANY'); - --- CreateEnum -CREATE TYPE "AchievementType" AS ENUM ('TOTAL_POINTS', 'TOTAL_CHALLENGES', 'TOTAL_JOURNEYS', 'TOTAL_CHALLENGES_OR_JOURNEYS'); - --- CreateEnum -CREATE TYPE "DifficultyMode" AS ENUM ('EASY', 'NORMAL', 'HARD'); - --- CreateEnum -CREATE TYPE "SessionLogEvent" AS ENUM ('JOIN_GROUP', 'LEAVE_GROUP', 'LOGIN_USER', 'CREATE_USER', 'DELETE_USER', 'EDIT_USERNAME', 'SELECT_EVENT', 'DELETE_EVENT', 'SET_CHALLENGE', 'DELETE_CHALLENGE', 'COMPLETE_CHALLENGE', 'DISCONNECT'); - --- DropForeignKey -ALTER TABLE "EventBase" DROP CONSTRAINT "EventBase_defaultChallengeId_fkey"; - --- DropForeignKey -ALTER TABLE "EventReward" DROP CONSTRAINT "EventReward_eventId_fkey"; - --- DropForeignKey -ALTER TABLE "EventReward" DROP CONSTRAINT "EventReward_userId_fkey"; - --- DropForeignKey -ALTER TABLE "Organization" DROP CONSTRAINT "Organization_defaultEventId_fkey"; - --- DropForeignKey -ALTER TABLE "_events" DROP CONSTRAINT "_events_A_fkey"; - --- DropForeignKey -ALTER TABLE "_events" DROP CONSTRAINT "_events_B_fkey"; - --- DropForeignKey -ALTER TABLE "_manager" DROP CONSTRAINT "_manager_A_fkey"; - --- DropForeignKey -ALTER TABLE "_manager" DROP CONSTRAINT "_manager_B_fkey"; - --- DropForeignKey -ALTER TABLE "_participant" DROP CONSTRAINT "_participant_A_fkey"; - --- DropForeignKey -ALTER TABLE "_participant" DROP CONSTRAINT "_participant_B_fkey"; - --- DropForeignKey -ALTER TABLE "_player" DROP CONSTRAINT "_player_A_fkey"; - --- DropForeignKey -ALTER TABLE "_player" DROP CONSTRAINT "_player_B_fkey"; - --- DropIndex -DROP INDEX "EventBase_defaultChallengeId_key"; - --- AlterTable -ALTER TABLE "EventBase" DROP COLUMN "defaultChallengeId", -DROP COLUMN "minimumScore", -DROP COLUMN "rewardType", -ADD COLUMN "difficulty" "DifficultyMode" NOT NULL, -ADD COLUMN "latitude" DOUBLE PRECISION NOT NULL, -ADD COLUMN "longitude" DOUBLE PRECISION NOT NULL, -ADD COLUMN "timeLimitation" "TimeLimitationType" NOT NULL; - --- AlterTable -ALTER TABLE "Organization" DROP COLUMN "defaultEventId"; - --- AlterTable -ALTER TABLE "User" ADD COLUMN "enrollmentType" "EnrollmentType" NOT NULL, -ADD COLUMN "isBanned" BOOLEAN NOT NULL DEFAULT false, -ADD COLUMN "year" TEXT NOT NULL; - --- DropTable -DROP TABLE "EventReward"; - --- DropTable -DROP TABLE "_events"; - --- DropTable -DROP TABLE "_manager"; - --- DropTable -DROP TABLE "_participant"; - --- DropTable -DROP TABLE "_player"; - --- DropEnum -DROP TYPE "EventRewardType"; - --- CreateTable -CREATE TABLE "SessionLogEntry" ( - "id" TEXT NOT NULL, - "eventType" "SessionLogEvent" NOT NULL, - "data" TEXT NOT NULL, - "timestamp" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "userId" TEXT, - - CONSTRAINT "SessionLogEntry_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "Achievement" ( - "id" TEXT NOT NULL, - "requiredPoints" INTEGER NOT NULL, - "name" TEXT NOT NULL, - "description" TEXT NOT NULL, - "imageUrl" TEXT NOT NULL, - "linkedEventId" TEXT, - "locationType" "LocationType" NOT NULL, - "achievementType" "AchievementType" NOT NULL, - - CONSTRAINT "Achievement_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "AchievementTracker" ( - "id" TEXT NOT NULL, - "progress" INTEGER NOT NULL, - "dateComplete" TIMESTAMP(3), - "achievementId" TEXT NOT NULL, - "userId" TEXT NOT NULL, - - CONSTRAINT "AchievementTracker_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "_EventBaseToUser" ( - "A" TEXT NOT NULL, - "B" TEXT NOT NULL -); - --- CreateTable -CREATE TABLE "_eventOrgs" ( - "A" TEXT NOT NULL, - "B" TEXT NOT NULL -); - --- CreateTable -CREATE TABLE "_prevChallengeParticipant" ( - "A" INTEGER NOT NULL, - "B" TEXT NOT NULL -); - --- CreateTable -CREATE TABLE "_orgToUser" ( - "A" TEXT NOT NULL, - "B" TEXT NOT NULL -); - --- CreateTable -CREATE TABLE "_orgManager" ( - "A" TEXT NOT NULL, - "B" TEXT NOT NULL -); - --- CreateIndex -CREATE UNIQUE INDEX "_EventBaseToUser_AB_unique" ON "_EventBaseToUser"("A", "B"); - --- CreateIndex -CREATE INDEX "_EventBaseToUser_B_index" ON "_EventBaseToUser"("B"); - --- CreateIndex -CREATE UNIQUE INDEX "_eventOrgs_AB_unique" ON "_eventOrgs"("A", "B"); - --- CreateIndex -CREATE INDEX "_eventOrgs_B_index" ON "_eventOrgs"("B"); - --- CreateIndex -CREATE UNIQUE INDEX "_prevChallengeParticipant_AB_unique" ON "_prevChallengeParticipant"("A", "B"); - --- CreateIndex -CREATE INDEX "_prevChallengeParticipant_B_index" ON "_prevChallengeParticipant"("B"); - --- CreateIndex -CREATE UNIQUE INDEX "_orgToUser_AB_unique" ON "_orgToUser"("A", "B"); - --- CreateIndex -CREATE INDEX "_orgToUser_B_index" ON "_orgToUser"("B"); - --- CreateIndex -CREATE UNIQUE INDEX "_orgManager_AB_unique" ON "_orgManager"("A", "B"); - --- CreateIndex -CREATE INDEX "_orgManager_B_index" ON "_orgManager"("B"); - --- AddForeignKey -ALTER TABLE "SessionLogEntry" ADD CONSTRAINT "SessionLogEntry_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "Achievement" ADD CONSTRAINT "Achievement_linkedEventId_fkey" FOREIGN KEY ("linkedEventId") REFERENCES "EventBase"("id") ON DELETE SET NULL ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "AchievementTracker" ADD CONSTRAINT "AchievementTracker_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "AchievementTracker" ADD CONSTRAINT "AchievementTracker_achievementId_fkey" FOREIGN KEY ("achievementId") REFERENCES "Achievement"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "_EventBaseToUser" ADD CONSTRAINT "_EventBaseToUser_A_fkey" FOREIGN KEY ("A") REFERENCES "EventBase"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "_EventBaseToUser" ADD CONSTRAINT "_EventBaseToUser_B_fkey" FOREIGN KEY ("B") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "_eventOrgs" ADD CONSTRAINT "_eventOrgs_A_fkey" FOREIGN KEY ("A") REFERENCES "EventBase"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "_eventOrgs" ADD CONSTRAINT "_eventOrgs_B_fkey" FOREIGN KEY ("B") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "_prevChallengeParticipant" ADD CONSTRAINT "_prevChallengeParticipant_A_fkey" FOREIGN KEY ("A") REFERENCES "PrevChallenge"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "_prevChallengeParticipant" ADD CONSTRAINT "_prevChallengeParticipant_B_fkey" FOREIGN KEY ("B") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "_orgToUser" ADD CONSTRAINT "_orgToUser_A_fkey" FOREIGN KEY ("A") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "_orgToUser" ADD CONSTRAINT "_orgToUser_B_fkey" FOREIGN KEY ("B") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "_orgManager" ADD CONSTRAINT "_orgManager_A_fkey" FOREIGN KEY ("A") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "_orgManager" ADD CONSTRAINT "_orgManager_B_fkey" FOREIGN KEY ("B") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/server/prisma/migrations/20240423223320_initial_migration/migration.sql b/server/prisma/migrations/20240423223320_initial_migration/migration.sql new file mode 100644 index 00000000..d482ba36 --- /dev/null +++ b/server/prisma/migrations/20240423223320_initial_migration/migration.sql @@ -0,0 +1,328 @@ +-- CreateEnum +CREATE TYPE "AuthType" AS ENUM ('GOOGLE', 'APPLE', 'DEVICE', 'NONE'); + +-- CreateEnum +CREATE TYPE "TimeLimitationType" AS ENUM ('LIMITED_TIME', 'PERPETUAL'); + +-- CreateEnum +CREATE TYPE "EnrollmentType" AS ENUM ('UNDERGRADUATE', 'GRADUATE', 'FACULTY', 'ALUMNI', 'GUEST'); + +-- CreateEnum +CREATE TYPE "LocationType" AS ENUM ('ENG_QUAD', 'ARTS_QUAD', 'AG_QUAD', 'NORTH_CAMPUS', 'WEST_CAMPUS', 'COLLEGETOWN', 'ITHACA_COMMONS', 'ANY'); + +-- CreateEnum +CREATE TYPE "AchievementType" AS ENUM ('TOTAL_POINTS', 'TOTAL_CHALLENGES', 'TOTAL_JOURNEYS', 'TOTAL_CHALLENGES_OR_JOURNEYS'); + +-- CreateEnum +CREATE TYPE "DifficultyMode" AS ENUM ('EASY', 'NORMAL', 'HARD'); + +-- CreateEnum +CREATE TYPE "SessionLogEvent" AS ENUM ('JOIN_GROUP', 'LEAVE_GROUP', 'LOGIN_USER', 'CREATE_USER', 'DELETE_USER', 'EDIT_USERNAME', 'SELECT_EVENT', 'DELETE_EVENT', 'SET_CHALLENGE', 'DELETE_CHALLENGE', 'COMPLETE_CHALLENGE', 'DISCONNECT'); + +-- CreateEnum +CREATE TYPE "OrganizationSpecialUsage" AS ENUM ('DEVICE_LOGIN', 'CORNELL_LOGIN', 'NONE'); + +-- CreateEnum +CREATE TYPE "EventCategoryType" AS ENUM ('FOOD', 'NATURE', 'HISTORICAL', 'CAFE', 'DININGHALL', 'DORM'); + +-- CreateTable +CREATE TABLE "User" ( + "id" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "subject" TEXT NOT NULL DEFAULT 'User', + "authToken" TEXT NOT NULL, + "authType" "AuthType" NOT NULL, + "username" TEXT NOT NULL, + "year" TEXT NOT NULL, + "email" TEXT NOT NULL, + "hashedRefreshToken" TEXT NOT NULL, + "administrator" BOOLEAN NOT NULL, + "enrollmentType" "EnrollmentType" NOT NULL, + "score" INTEGER NOT NULL, + "isBanned" BOOLEAN NOT NULL DEFAULT false, + "groupId" TEXT NOT NULL, + "isRanked" BOOLEAN NOT NULL DEFAULT true, + + CONSTRAINT "User_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Group" ( + "id" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "subject" TEXT NOT NULL DEFAULT 'Group', + "friendlyId" TEXT NOT NULL, + "hostId" TEXT, + "curEventId" TEXT NOT NULL, + + CONSTRAINT "Group_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Challenge" ( + "id" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "subject" TEXT NOT NULL DEFAULT 'Challenge', + "linkedEventId" TEXT, + "eventIndex" INTEGER NOT NULL, + "name" TEXT NOT NULL, + "location" "LocationType" NOT NULL, + "description" TEXT NOT NULL, + "points" INTEGER NOT NULL, + "imageUrl" TEXT NOT NULL, + "latitude" DOUBLE PRECISION NOT NULL, + "longitude" DOUBLE PRECISION NOT NULL, + "awardingRadius" DOUBLE PRECISION NOT NULL, + "closeRadius" DOUBLE PRECISION NOT NULL, + + CONSTRAINT "Challenge_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "EventBase" ( + "id" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "subject" TEXT NOT NULL DEFAULT 'EventBase', + "requiredMembers" INTEGER NOT NULL, + "name" TEXT NOT NULL, + "description" TEXT NOT NULL, + "timeLimitation" "TimeLimitationType" NOT NULL, + "indexable" BOOLEAN NOT NULL, + "endTime" TIMESTAMP(3) NOT NULL, + "latitude" DOUBLE PRECISION NOT NULL, + "longitude" DOUBLE PRECISION NOT NULL, + "difficulty" "DifficultyMode" NOT NULL, + "category" "EventCategoryType" NOT NULL, + + CONSTRAINT "EventBase_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "EventTracker" ( + "id" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "subject" TEXT NOT NULL DEFAULT 'EventTracker', + "userId" TEXT NOT NULL, + "score" INTEGER NOT NULL, + "hintsUsed" INTEGER NOT NULL, + "isRankedForEvent" BOOLEAN NOT NULL DEFAULT true, + "eventId" TEXT NOT NULL, + "curChallengeId" TEXT NOT NULL, + + CONSTRAINT "EventTracker_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "PrevChallenge" ( + "id" SERIAL NOT NULL, + "subject" TEXT NOT NULL DEFAULT 'PrevChallenge', + "userId" TEXT NOT NULL, + "challengeId" TEXT NOT NULL, + "trackerId" TEXT NOT NULL, + "hintsUsed" INTEGER NOT NULL, + "timestamp" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "PrevChallenge_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Organization" ( + "id" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "subject" TEXT NOT NULL DEFAULT 'Organization', + "name" TEXT NOT NULL, + "accessCode" TEXT NOT NULL, + "specialUsage" "OrganizationSpecialUsage" NOT NULL, + + CONSTRAINT "Organization_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "SessionLogEntry" ( + "id" TEXT NOT NULL, + "subject" TEXT NOT NULL DEFAULT 'SessionLogEntry', + "eventType" "SessionLogEvent" NOT NULL, + "data" TEXT NOT NULL, + "timestamp" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "userId" TEXT, + + CONSTRAINT "SessionLogEntry_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Achievement" ( + "id" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "subject" TEXT NOT NULL DEFAULT 'Achievement', + "requiredPoints" INTEGER NOT NULL, + "name" TEXT NOT NULL, + "description" TEXT NOT NULL, + "imageUrl" TEXT NOT NULL, + "linkedEventId" TEXT, + "locationType" "LocationType" NOT NULL, + "achievementType" "AchievementType" NOT NULL, + + CONSTRAINT "Achievement_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "AchievementTracker" ( + "id" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "subject" TEXT NOT NULL DEFAULT 'AchievementTracker', + "progress" INTEGER NOT NULL, + "dateComplete" TIMESTAMP(3), + "achievementId" TEXT NOT NULL, + "userId" TEXT NOT NULL, + + CONSTRAINT "AchievementTracker_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "_EventBaseToUser" ( + "A" TEXT NOT NULL, + "B" TEXT NOT NULL +); + +-- CreateTable +CREATE TABLE "_eventOrgs" ( + "A" TEXT NOT NULL, + "B" TEXT NOT NULL +); + +-- CreateTable +CREATE TABLE "_prevChallengeParticipant" ( + "A" INTEGER NOT NULL, + "B" TEXT NOT NULL +); + +-- CreateTable +CREATE TABLE "_orgToUser" ( + "A" TEXT NOT NULL, + "B" TEXT NOT NULL +); + +-- CreateTable +CREATE TABLE "_orgManager" ( + "A" TEXT NOT NULL, + "B" TEXT NOT NULL +); + +-- CreateIndex +CREATE UNIQUE INDEX "User_authToken_key" ON "User"("authToken"); + +-- CreateIndex +CREATE UNIQUE INDEX "Group_friendlyId_key" ON "Group"("friendlyId"); + +-- CreateIndex +CREATE UNIQUE INDEX "Group_hostId_key" ON "Group"("hostId"); + +-- CreateIndex +CREATE UNIQUE INDEX "_EventBaseToUser_AB_unique" ON "_EventBaseToUser"("A", "B"); + +-- CreateIndex +CREATE INDEX "_EventBaseToUser_B_index" ON "_EventBaseToUser"("B"); + +-- CreateIndex +CREATE UNIQUE INDEX "_eventOrgs_AB_unique" ON "_eventOrgs"("A", "B"); + +-- CreateIndex +CREATE INDEX "_eventOrgs_B_index" ON "_eventOrgs"("B"); + +-- CreateIndex +CREATE UNIQUE INDEX "_prevChallengeParticipant_AB_unique" ON "_prevChallengeParticipant"("A", "B"); + +-- CreateIndex +CREATE INDEX "_prevChallengeParticipant_B_index" ON "_prevChallengeParticipant"("B"); + +-- CreateIndex +CREATE UNIQUE INDEX "_orgToUser_AB_unique" ON "_orgToUser"("A", "B"); + +-- CreateIndex +CREATE INDEX "_orgToUser_B_index" ON "_orgToUser"("B"); + +-- CreateIndex +CREATE UNIQUE INDEX "_orgManager_AB_unique" ON "_orgManager"("A", "B"); + +-- CreateIndex +CREATE INDEX "_orgManager_B_index" ON "_orgManager"("B"); + +-- AddForeignKey +ALTER TABLE "User" ADD CONSTRAINT "User_groupId_fkey" FOREIGN KEY ("groupId") REFERENCES "Group"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Group" ADD CONSTRAINT "Group_hostId_fkey" FOREIGN KEY ("hostId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Group" ADD CONSTRAINT "Group_curEventId_fkey" FOREIGN KEY ("curEventId") REFERENCES "EventBase"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Challenge" ADD CONSTRAINT "Challenge_linkedEventId_fkey" FOREIGN KEY ("linkedEventId") REFERENCES "EventBase"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "EventTracker" ADD CONSTRAINT "EventTracker_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "EventTracker" ADD CONSTRAINT "EventTracker_eventId_fkey" FOREIGN KEY ("eventId") REFERENCES "EventBase"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "EventTracker" ADD CONSTRAINT "EventTracker_curChallengeId_fkey" FOREIGN KEY ("curChallengeId") REFERENCES "Challenge"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "PrevChallenge" ADD CONSTRAINT "PrevChallenge_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "PrevChallenge" ADD CONSTRAINT "PrevChallenge_challengeId_fkey" FOREIGN KEY ("challengeId") REFERENCES "Challenge"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "PrevChallenge" ADD CONSTRAINT "PrevChallenge_trackerId_fkey" FOREIGN KEY ("trackerId") REFERENCES "EventTracker"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "SessionLogEntry" ADD CONSTRAINT "SessionLogEntry_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Achievement" ADD CONSTRAINT "Achievement_linkedEventId_fkey" FOREIGN KEY ("linkedEventId") REFERENCES "EventBase"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "AchievementTracker" ADD CONSTRAINT "AchievementTracker_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "AchievementTracker" ADD CONSTRAINT "AchievementTracker_achievementId_fkey" FOREIGN KEY ("achievementId") REFERENCES "Achievement"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "_EventBaseToUser" ADD CONSTRAINT "_EventBaseToUser_A_fkey" FOREIGN KEY ("A") REFERENCES "EventBase"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "_EventBaseToUser" ADD CONSTRAINT "_EventBaseToUser_B_fkey" FOREIGN KEY ("B") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "_eventOrgs" ADD CONSTRAINT "_eventOrgs_A_fkey" FOREIGN KEY ("A") REFERENCES "EventBase"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "_eventOrgs" ADD CONSTRAINT "_eventOrgs_B_fkey" FOREIGN KEY ("B") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "_prevChallengeParticipant" ADD CONSTRAINT "_prevChallengeParticipant_A_fkey" FOREIGN KEY ("A") REFERENCES "PrevChallenge"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "_prevChallengeParticipant" ADD CONSTRAINT "_prevChallengeParticipant_B_fkey" FOREIGN KEY ("B") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "_orgToUser" ADD CONSTRAINT "_orgToUser_A_fkey" FOREIGN KEY ("A") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "_orgToUser" ADD CONSTRAINT "_orgToUser_B_fkey" FOREIGN KEY ("B") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "_orgManager" ADD CONSTRAINT "_orgManager_A_fkey" FOREIGN KEY ("A") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "_orgManager" ADD CONSTRAINT "_orgManager_B_fkey" FOREIGN KEY ("B") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/server/prisma/schema.prisma b/server/prisma/schema.prisma index ea1f86c6..5b3d408a 100644 --- a/server/prisma/schema.prisma +++ b/server/prisma/schema.prisma @@ -82,6 +82,9 @@ model User { authType AuthType username String year String + college String + major String + interests String[] email String hashedRefreshToken String administrator Boolean diff --git a/server/src/achievement/achievement.e2e-spec.ts b/server/src/achievement/achievement.e2e-spec.ts index 872a8d7f..39122bb7 100644 --- a/server/src/achievement/achievement.e2e-spec.ts +++ b/server/src/achievement/achievement.e2e-spec.ts @@ -83,6 +83,9 @@ describe('AchievementModule E2E', () => { 'test@gmail.com', 'test', '2025', + 'Engineering', + 'Computer Science', + ['Nature'], 0, 0, AuthType.DEVICE, diff --git a/server/src/achievement/achievement.service.ts b/server/src/achievement/achievement.service.ts index 5732ea1e..60ba2868 100644 --- a/server/src/achievement/achievement.service.ts +++ b/server/src/achievement/achievement.service.ts @@ -170,7 +170,7 @@ export class AchievementService { await this.clientService.sendProtected( 'updateAchievementData', - target?.id ?? achievement.id, + target ?? achievement.id, dto, { id: achievement.id, diff --git a/server/src/auth/auth.gateway.ts b/server/src/auth/auth.gateway.ts index 9b263958..018be145 100644 --- a/server/src/auth/auth.gateway.ts +++ b/server/src/auth/auth.gateway.ts @@ -15,7 +15,7 @@ export class AuthGateway implements OnGatewayConnection { const user = await this.authService.userByToken(token); if (user) { client.data['userId'] = user.id; - client.join(user.id); + client.join('user/' + user.id); } else { client.disconnect(true); } diff --git a/server/src/auth/auth.service.ts b/server/src/auth/auth.service.ts index 89791071..a3088fe7 100644 --- a/server/src/auth/auth.service.ts +++ b/server/src/auth/auth.service.ts @@ -126,13 +126,17 @@ export class AuthService { let user = await this.userService.byAuth(authType, idToken.id); const isDevWhileDevice = process.env.DEVELOPMENT === 'true' || authType !== AuthType.DEVICE; - if (!user) { + + if (!user && !req.noRegister) { user = await this.userService.register( idToken.email, - req.username ?? '', - req.year ?? '', - req.lat, - req.long, + req.username, + req.year ?? '2000', + req.college ?? '', + req.major ?? '', + req.interests?.split(',') ?? [], + req.latF ?? 0, + req.longF ?? 0, authType, idToken.id, req.enrollmentType, diff --git a/server/src/auth/login.dto.ts b/server/src/auth/login.dto.ts index 6b2a15cd..edc385d7 100644 --- a/server/src/auth/login.dto.ts +++ b/server/src/auth/login.dto.ts @@ -4,10 +4,14 @@ */ export interface LoginDto { idToken: string; - lat: number; - long: number; + noRegister: boolean; + latF?: number; + longF?: number; username?: string; year?: string; + college?: string; + major?: string; + interests?: string; aud?: 'android' | 'ios' | 'web'; enrollmentType: 'UNDERGRADUATE' | 'GRADUATE' | 'FACULTY' | 'ALUMNI' | 'GUEST'; } diff --git a/server/src/casl/casl-ability.factory.ts b/server/src/casl/casl-ability.factory.ts index 78c863b0..42650332 100644 --- a/server/src/casl/casl-ability.factory.ts +++ b/server/src/casl/casl-ability.factory.ts @@ -119,9 +119,14 @@ export class CaslAbilityFactory { return build(); } - can(Action.Manage, 'User', ['enrollmentType', 'username', 'year'], { - id: user.id, - }); + can( + Action.Manage, + 'User', + ['enrollmentType', 'username', 'college', 'major', 'year'], + { + id: user.id, + }, + ); can( Action.Read, @@ -151,6 +156,7 @@ export class CaslAbilityFactory { }, { OR: [ + { eventIndex: 0 }, { completions: { some: { userId: user.id }, @@ -195,8 +201,6 @@ export class CaslAbilityFactory { ]; cannot(Action.Read, 'Challenge', latlongNames, { - // names come from DTO - // hide lat long from users that do not have an active tracker which is their current event activeTrackers: { none: { user: { id: user.id }, @@ -205,6 +209,7 @@ export class CaslAbilityFactory { }, }, }, + eventIndex: { not: 0 }, }); can(Action.Manage, 'Challenge', { diff --git a/server/src/challenge/challenge.dto.ts b/server/src/challenge/challenge.dto.ts index 3b3c2ea2..a3c0e10a 100644 --- a/server/src/challenge/challenge.dto.ts +++ b/server/src/challenge/challenge.dto.ts @@ -47,17 +47,3 @@ export interface RequestEventTrackerDataDto { export interface SetCurrentChallengeDto { challengeId: string; } - -/** DTO for user in updateLeaderData */ -export interface LeaderDto { - userId: string; - username: string; - score: number; -} - -/** DTO for updateLeaderData */ -export interface UpdateLeaderDataDto { - eventId: string; - offset: number; - users: LeaderDto[]; -} diff --git a/server/src/challenge/challenge.e2e-spec.ts b/server/src/challenge/challenge.e2e-spec.ts index 02738a95..e1404456 100644 --- a/server/src/challenge/challenge.e2e-spec.ts +++ b/server/src/challenge/challenge.e2e-spec.ts @@ -72,6 +72,9 @@ describe('ChallengeModule E2E', () => { 'test@gmail.com', 'test', '2025', + 'Engineering', + 'Computer Science', + ['Nature'], 0, 0, AuthType.DEVICE, diff --git a/server/src/challenge/challenge.gateway.ts b/server/src/challenge/challenge.gateway.ts index f73a1953..5ff4bbc4 100644 --- a/server/src/challenge/challenge.gateway.ts +++ b/server/src/challenge/challenge.gateway.ts @@ -105,19 +105,6 @@ export class ChallengeGateway { } } - @SubscribeMessage('requestGlobalLeaderData') - async requestGlobalLeaderData( - @CallingUser() user: User, - @MessageBody() data: RequestGlobalLeaderDataDto, - ) { - await this.eventService.emitUpdateLeaderData( - data.offset, - Math.min(data.count, 1024), - null, - user, - ); - } - @SubscribeMessage('updateChallengeData') async updateChallengeData( @UserAbility() ability: AppAbility, diff --git a/server/src/challenge/challenge.service.ts b/server/src/challenge/challenge.service.ts index 78bf45ec..4352d17d 100644 --- a/server/src/challenge/challenge.service.ts +++ b/server/src/challenge/challenge.service.ts @@ -101,7 +101,7 @@ export class ChallengeService { const eventTracker: EventTracker = await this.eventService.getCurrentEventTrackerForUser(user); - // const achievementTracker : AchievementTracker = + // const achievementTracker : AchievementTracker = // await this.achievementService.getAchievementsByIdsForAbility(user.ability, [eventTracker.id]); const alreadyDone = @@ -138,12 +138,12 @@ export class ChallengeService { const totalScore = curChallenge.points - 25 * eventTracker.hintsUsed; - await this.prisma.user.update({ + const newUser = await this.prisma.user.update({ where: { id: user.id }, data: { score: { increment: totalScore } }, }); - await this.prisma.eventTracker.update({ + const newEvTracker = await this.prisma.eventTracker.update({ where: { id: eventTracker.id }, data: { score: { increment: totalScore }, @@ -159,18 +159,30 @@ export class ChallengeService { ); // check if the challenge is part of a journey - const isJourney = (await this.prisma.prevChallenge.count({ - where: { - userId: user.id, - challengeId: eventTracker.curChallengeId, - trackerId: eventTracker.id, - }, - })) === (await this.prisma.eventTracker.count({ - where: { id: eventTracker.id }, // CHECK - })); + const isJourney = + (await this.prisma.prevChallenge.count({ + where: { + userId: user.id, + challengeId: eventTracker.curChallengeId, + trackerId: eventTracker.id, + }, + })) === + (await this.prisma.eventTracker.count({ + where: { id: eventTracker.id }, // CHECK + })); - await this.achievementService.checkAchievementProgress(user, challengeId, isJourney); - + await this.achievementService.checkAchievementProgress( + user, + challengeId, + isJourney, + ); + + await this.eventService.emitUpdateLeaderPosition({ + playerId: newUser.id, + newTotalScore: newUser.score, + newEventScore: newEvTracker.score, + eventId: newEvTracker.eventId, + }); return true; } @@ -254,7 +266,7 @@ export class ChallengeService { await this.clientService.sendProtected( 'updateChallengeData', - target?.id ?? challenge.id, + target ?? challenge.id, dto, { id: challenge.id, diff --git a/server/src/client/client.service.ts b/server/src/client/client.service.ts index 539bd642..b84fc3b0 100644 --- a/server/src/client/client.service.ts +++ b/server/src/client/client.service.ts @@ -12,11 +12,13 @@ import { PermittedFieldsOptions, permittedFieldsOf } from '@casl/ability/extra'; import { Action } from '../casl/action.enum'; import { Subjects } from '@casl/prisma'; import { UpdateUserDataDto } from '../user/user.dto'; +import { UpdateChallengeDataDto } from '../challenge/challenge.dto'; import { - UpdateChallengeDataDto, + EventTrackerDto, + UpdateEventDataDto, + UpdateLeaderPositionDto, UpdateLeaderDataDto, -} from '../challenge/challenge.dto'; -import { EventTrackerDto, UpdateEventDataDto } from '../event/event.dto'; +} from '../event/event.dto'; import { GroupInviteDto, UpdateGroupDataDto } from '../group/group.dto'; import { UpdateOrganizationDataDto } from '../organization/organization.dto'; import { @@ -37,6 +39,7 @@ export type ClientApiDef = { groupInvitation: GroupInviteDto; updateGroupData: UpdateGroupDataDto; updateOrganizationData: UpdateOrganizationDataDto; + updateLeaderPosition: UpdateLeaderPositionDto; }; @Injectable() @@ -65,7 +68,7 @@ export class ClientService { message, }; - await this.sendProtected('updateErrorData', user.id, dto); + await this.sendProtected('updateErrorData', user, dto); } async getAffectedUsers(target: string) { @@ -83,13 +86,22 @@ export class ClientService { return users; } - async sendEvent(users: string[], event: string, dto: TDto) { - this.gateway.server.to(users).emit(event, dto); + async sendEvent( + users: string[] | null, + event: string, + dto: TDto, + ) { + if (process.env.TESTING_E2E === 'true') { + return; + } + + if (users) this.gateway.server.to(users).emit(event, dto); + else this.gateway.server.emit(event, dto); } async sendProtected( event: keyof ClientApiDef, - target: string, + target: string | User | null, dto: ClientApiDef[typeof event] & TDto, resource?: { id: string; @@ -100,13 +112,18 @@ export class ClientService { }; }, ) { + if (!target) { + this.sendEvent(null, event, dto); + return; + } + + const room = target instanceof Object ? 'user/' + target.id : target; if (!resource) { - this.gateway.server.to(target).emit(event, dto); + this.gateway.server.to(room).emit(event, dto); } else { - this.gateway.server.in(target).socketsJoin(resource.id); - + this.gateway.server.in(room).socketsJoin(resource.id); // Find all targeted users - const users = await this.getAffectedUsers(target); + const users = await this.getAffectedUsers(room); for (const user of users) { const ability = this.abilityFactory.createForUser(user); @@ -126,9 +143,9 @@ export class ClientService { ...dto, [resource.dtoField]: accessibleObj, }; - await this.sendEvent([user.id], event, newDto); + await this.sendEvent(['user/' + user.id], event, newDto); } else { - await this.sendEvent([user.id], event, accessibleObj); + await this.sendEvent(['user/' + user.id], event, accessibleObj); } } } diff --git a/server/src/event/event.dto.ts b/server/src/event/event.dto.ts index 6f8ea353..b1624325 100644 --- a/server/src/event/event.dto.ts +++ b/server/src/event/event.dto.ts @@ -1,7 +1,9 @@ -/** DTO for requestAllEventData */ -export interface RequestAllEventDataDto { - offset: number; - count: number; +/** DTO for RequestFilteredEvents */ +export interface RequestFilteredEventsDto { + difficulty: string[]; + location: string[]; + category: string[]; + filterId: string[]; } /** DTO for requestEventData */ @@ -13,6 +15,27 @@ export interface RequestEventDataDto { export interface RequestEventLeaderDataDto { offset: number; count: number; + eventId?: string; +} + +/** DTO for user in updateLeaderData */ +export interface LeaderDto { + userId: string; + username: string; + score: number; +} + +/** DTO for updateLeaderData */ +export interface UpdateLeaderDataDto { + eventId?: string; + offset: number; + users: LeaderDto[]; +} + +export interface UpdateLeaderPositionDto { + playerId: string; + newTotalScore: number; + newEventScore: number; eventId: string; } diff --git a/server/src/event/event.e2e-spec.ts b/server/src/event/event.e2e-spec.ts index b0c01acd..5ad5459f 100644 --- a/server/src/event/event.e2e-spec.ts +++ b/server/src/event/event.e2e-spec.ts @@ -27,7 +27,10 @@ describe('EventModule E2E', () => { let moduleRef: TestingModule; let fullAbility: AppAbility; - let sendEventMock: jest.SpyInstance, [string[], string, any]>; + let sendEventMock: jest.SpyInstance< + Promise, + [string[] | null, string, any] + >; let chalGateway: ChallengeGateway; let evGateway: EventGateway; @@ -80,6 +83,9 @@ describe('EventModule E2E', () => { 'player1@cornell.edu', 'player1', '2024', + 'Engineering', + 'Computer Science', + ['Nature'], 0, 0, AuthType.DEVICE, diff --git a/server/src/event/event.gateway.ts b/server/src/event/event.gateway.ts index fafe4003..7e37c96c 100644 --- a/server/src/event/event.gateway.ts +++ b/server/src/event/event.gateway.ts @@ -12,12 +12,13 @@ import { UserGuard } from '../auth/jwt-auth.guard'; import { UseGuards } from '@nestjs/common'; import { EventBase, TimeLimitationType, User } from '@prisma/client'; import { - EventDto, - RequestAllEventDataDto, + // EventDto, + // RequestAllEventDataDto, RequestEventDataDto, RequestEventLeaderDataDto, UpdateEventDataDto, RequestRecommendedEventsDto, + RequestFilteredEventsDto, UseEventTrackerHintDto, } from './event.dto'; import { RequestEventTrackerDataDto } from '../challenge/challenge.dto'; @@ -59,6 +60,24 @@ export class EventGateway { } } + @SubscribeMessage('requestFilteredEventIds') + async requestFilteredEventIds( + @UserAbility() ability: AppAbility, + @CallingUser() user: User, + @MessageBody() data: RequestFilteredEventsDto, + ) { + const evs = await this.eventService.getEventsByIdsForAbility( + ability, + data.filterId, + ); + + for (const ev of evs) { + if (ev.difficulty == data.difficulty[0]) { + await this.eventService.emitUpdateEventData(ev, false, user); + } + } + } + @SubscribeMessage('requestRecommendedEvents') async requestRecommendedEvents( @CallingUser() user: User, @@ -76,8 +95,11 @@ export class EventGateway { @CallingUser() user: User, @MessageBody() data: RequestEventLeaderDataDto, ) { - const ev = await this.eventService.getEventById(data.eventId); - if (!ev) { + const ev = data.eventId + ? await this.eventService.getEventById(data.eventId) + : null; + + if (!ev && data.eventId) { await this.clientService.emitErrorData( user, 'Cannot find requested event!', @@ -102,7 +124,6 @@ export class EventGateway { user, data.trackedEvents, ); - for (const tracker of trackers) { await this.eventService.emitUpdateEventTracker(tracker, user); } diff --git a/server/src/event/event.service.ts b/server/src/event/event.service.ts index 2f2faca3..d32fbe1c 100644 --- a/server/src/event/event.service.ts +++ b/server/src/event/event.service.ts @@ -7,7 +7,7 @@ import { EventTracker, User, } from '@prisma/client'; -import { LeaderDto, UpdateLeaderDataDto } from '../challenge/challenge.dto'; +import { LeaderDto, UpdateLeaderDataDto } from './event.dto'; import { v4 } from 'uuid'; import { ClientService } from '../client/client.service'; import { @@ -23,6 +23,7 @@ import { RequestRecommendedEventsDto, UpdateEventTrackerDataDto, EventCategoryDto, + UpdateLeaderPositionDto, } from './event.dto'; import { AppAbility, CaslAbilityFactory } from '../casl/casl-ability.factory'; import { accessibleBy } from '@casl/prisma'; @@ -48,6 +49,7 @@ export class EventService { ability: AppAbility, ids?: string[], ): Promise { + // console.log("Ids are " + ids) return await this.prisma.eventBase.findMany({ where: { AND: [ @@ -264,7 +266,7 @@ export class EventService { description: ev.description, category: ev.category as EventCategoryDto, timeLimitation: - ev.timeLimitation == TimeLimitationType.LIMITED_TIME + ev.timeLimitation === TimeLimitationType.LIMITED_TIME ? 'LIMITED_TIME' : 'PERPETUAL', endTime: ev.endTime.toUTCString(), @@ -311,7 +313,7 @@ export class EventService { const dto = await this.dtoForEventTracker(tracker); await this.clientService.sendProtected( 'updateEventTrackerData', - target?.id ?? tracker.id, + target ?? tracker.id, dto, { id: tracker.id, @@ -339,7 +341,7 @@ export class EventService { await this.clientService.sendProtected( 'updateEventData', - target?.id ?? ev.id, + target ?? ev.id, dto, { id: ev.id, @@ -350,6 +352,14 @@ export class EventService { ); } + async emitUpdateLeaderPosition(updateDto: UpdateLeaderPositionDto) { + await this.clientService.sendProtected( + 'updateLeaderPosition', + null, + updateDto, + ); + } + async emitUpdateLeaderData( offset: number, count: number, @@ -393,12 +403,12 @@ export class EventService { } const dto: UpdateLeaderDataDto = { - eventId: event?.id ?? '', + eventId: event?.id, offset, users: leaderData, }; - await this.clientService.sendProtected('updateLeaderData', target.id, dto); + await this.clientService.sendProtected('updateLeaderData', target, dto); } async upsertEventFromDto(ability: AppAbility, event: EventDto) { diff --git a/server/src/group/group.service.ts b/server/src/group/group.service.ts index 43b2b05c..c129aa6c 100644 --- a/server/src/group/group.service.ts +++ b/server/src/group/group.service.ts @@ -311,7 +311,7 @@ export class GroupService { await this.clientService.sendProtected( 'updateGroupData', - target?.id ?? group.id, + target ?? group.id, dto, { id: group.id, @@ -335,7 +335,7 @@ export class GroupService { if (targetUser) { await this.clientService.sendProtected( 'groupInvitation', - targetUser.id, + targetUser, dto, ); } else { diff --git a/server/src/organization/organization.e2e-spec.ts b/server/src/organization/organization.e2e-spec.ts index c90d2564..cc84a1d1 100644 --- a/server/src/organization/organization.e2e-spec.ts +++ b/server/src/organization/organization.e2e-spec.ts @@ -31,7 +31,7 @@ import { ChallengeLocationDto, } from '../challenge/challenge.dto'; -type DtoLastCall = [string[], string, T]; +type DtoLastCall = [string[] | null, string, T]; describe('OrganizationModule E2E', () => { let app: INestApplication; @@ -69,7 +69,15 @@ describe('OrganizationModule E2E', () => { let exEv: EventBase; let exChal: Challenge; - let sendEventMock: jest.SpyInstance, [string[], string, any]>; + let exOrg2: Organization; + let exEv2: EventBase; + let exChal1: Challenge; + let exChal2: Challenge; + + let sendEventMock: jest.SpyInstance< + Promise, + [string[] | null, string, any] + >; let affectedUsers: User[] = []; beforeAll(async () => { @@ -102,7 +110,7 @@ describe('OrganizationModule E2E', () => { }))!; exEv = (await eventService.upsertEventFromDto(fullAbility, { - id: '', + id: 'ev1', initialOrganizationId: exOrg.id, }))!; @@ -112,10 +120,36 @@ describe('OrganizationModule E2E', () => { location: ChallengeLocationDto.ARTS_QUAD, }))!; + exOrg2 = (await orgService.upsertOrganizationFromDto(fullAbility, { + id: '', + }))!; + + exEv2 = (await eventService.upsertEventFromDto(fullAbility, { + id: 'ev2', + initialOrganizationId: exOrg2.id, + }))!; + + exChal1 = (await challengeService.upsertChallengeFromDto(fullAbility, { + id: 'exchal1', + name: 'exchal1', + linkedEventId: exEv2.id, + location: ChallengeLocationDto.ARTS_QUAD, + }))!; + + exChal2 = (await challengeService.upsertChallengeFromDto(fullAbility, { + id: 'exchal2', + name: 'exchal2', + linkedEventId: exEv2.id, + location: ChallengeLocationDto.ENG_QUAD, + }))!; + managerUser = await userService.register( 'manager@cornell.edu', 'manager', '2024', + 'Engineering', + 'Computer Science', + ['Nature'], 0, 0, AuthType.DEVICE, @@ -127,6 +161,9 @@ describe('OrganizationModule E2E', () => { 'basic@cornell.edu', 'basic', '2024', + 'Engineering', + 'Computer Science', + ['Nature'], 0, 0, AuthType.DEVICE, @@ -152,7 +189,10 @@ describe('OrganizationModule E2E', () => { managerGroup = await groupService.getGroupForUser(managerUser); basicGroup = await groupService.getGroupForUser(basicUser); + // add manager user as manager to first org await orgService.addManager(fullAbility, 'manager@cornell.edu', exOrg.id); + // add basic user as member of second org + await orgService.joinOrganization(basicUser, exOrg2.accessCode); managerAbility = abilityFactory.createForUser(managerUser); basicAbility = abilityFactory.createForUser(basicUser); @@ -182,8 +222,8 @@ describe('OrganizationModule E2E', () => { sendEventMock.mock.lastCall; expect(ev).toEqual('updateUserData'); - expect(users).toContain(basicUser.id); - expect(users).not.toContain(managerUser.id); + expect(users).toContain('user/' + basicUser.id); + expect(users).not.toContain('user/' + managerUser.id); expect(dto.user.id).toEqual(basicUser.id); expect(dto.user.email).toEqual(basicUser.email); @@ -207,15 +247,32 @@ describe('OrganizationModule E2E', () => { expect(dto.user.username).toEqual('myNewUsername'); }); - it('Should not be able to see non-current challenge', async () => { - await groupService.setCurrentEvent(managerUser, exEv.id); + it('Should be able to see lat, long of first challenge in a non-current event', async () => { + await groupService.setCurrentEvent(basicUser, defaultEv.id); - affectedUsers.push(managerUser); - await chalGateway.requestChallengeData(managerAbility, managerUser, { - challenges: [defaultChal.id], + affectedUsers.push(basicUser); + await chalGateway.requestChallengeData(basicAbility, basicUser, { + challenges: [exChal1.id], }); - expect(sendEventMock.mock.lastCall).toBeUndefined(); + const [users, ev, dto]: DtoLastCall = + sendEventMock.mock.lastCall; + + expect(ev).toEqual('updateChallengeData'); + expect(dto.challenge.name).toBeUndefined(); + expect(dto.challenge.latF).toEqual(exChal1.latitude); + expect(dto.challenge.longF).toEqual(exChal1.longitude); + }); + + it('Should not be able to see lat, long of second challenge in a non-current evnet', async () => { + await groupService.setCurrentEvent(basicUser, defaultEv.id); + + affectedUsers.push(basicUser); + expect( + await chalGateway.requestChallengeData(basicAbility, basicUser, { + challenges: [exChal2.id], + }), + ).toBeFalsy; }); it('Should be able to see current challenge lat long but not name', async () => { @@ -234,7 +291,7 @@ describe('OrganizationModule E2E', () => { expect(dto.challenge.longF).toBeDefined(); }); - it('Should be able to see completed challenge name but not lat long', async () => { + it('Should be able to see completed challenge name lat and long', async () => { await groupService.setCurrentEvent(managerUser, defaultEv.id); await chalGateway.completedChallenge(managerUser, { challengeId: defaultChal.id, @@ -251,8 +308,8 @@ describe('OrganizationModule E2E', () => { expect(ev).toEqual('updateChallengeData'); expect(dto.challenge.name).toBeDefined(); - expect(dto.challenge.latF).toBeUndefined(); - expect(dto.challenge.longF).toBeUndefined(); + expect(dto.challenge.latF).toBeDefined(); + expect(dto.challenge.longF).toBeDefined(); }); it('Should not be able to set current event to event of not allowed org', async () => { diff --git a/server/src/organization/organization.service.ts b/server/src/organization/organization.service.ts index 1cb40f38..4780d687 100644 --- a/server/src/organization/organization.service.ts +++ b/server/src/organization/organization.service.ts @@ -87,10 +87,11 @@ export class OrganizationService { } // TODO: maybe move this to event.service in the future? - async makeDefaultEvent(orgId?: string) { + async makeDefaultEvent(orgId?: string, name?: string) { const ev = await this.prisma.eventBase.create({ data: { ...defaultEventData, + ...(name ? { name } : {}), usedIn: orgId ? { connect: { id: orgId } } : undefined, }, }); @@ -120,7 +121,12 @@ export class OrganizationService { }); if (defaultOrg === null) { - const ev = await this.makeDefaultEvent(); + const ev = await this.makeDefaultEvent( + undefined, + usage === OrganizationSpecialUsage.CORNELL_LOGIN + ? 'Cornell Event' + : 'Everyone Event', + ); defaultOrg = await this.prisma.organization.create({ data: { @@ -187,7 +193,7 @@ export class OrganizationService { await this.clientService.sendProtected( 'updateOrganizationData', - target?.id ?? organization.id, + target ?? organization.id, dto, { id: organization.id, @@ -354,10 +360,12 @@ export class OrganizationService { } async joinOrganization(user: User, code: string) { - const org = await this.prisma.organization.findFirstOrThrow({ + const org = await this.prisma.organization.findFirst({ where: { accessCode: code }, }); + if (!org) return; + await this.prisma.organization.update({ where: { id: org.id }, data: { members: { connect: { id: user.id } } }, diff --git a/server/src/user/user.dto.ts b/server/src/user/user.dto.ts index 896b595f..64b0a281 100644 --- a/server/src/user/user.dto.ts +++ b/server/src/user/user.dto.ts @@ -3,21 +3,6 @@ import { PrevChallenge } from '@prisma/client'; /** DTO for closeAccount */ export interface CloseAccountDto {} -/** DTO for setUsername */ -export interface SetUsernameDto { - newUsername: string; -} - -/** DTO for setMajor */ -export interface SetMajorDto { - newMajor: string; -} - -/** DTO for setGraduationYear */ -export interface SetGraduationYearDto { - newYear: string; -} - export interface BanUserDto { userId: string; isBanned: boolean; @@ -66,6 +51,9 @@ export interface UserDto { | 'GUEST'; email?: string; year?: string; + college?: string; + major?: string; + interests?: string[]; score?: number; isBanned?: boolean; groupId?: string; diff --git a/server/src/user/user.e2e-spec.ts b/server/src/user/user.e2e-spec.ts index 62c6fe31..f2aefe0e 100644 --- a/server/src/user/user.e2e-spec.ts +++ b/server/src/user/user.e2e-spec.ts @@ -36,6 +36,9 @@ describe('UserModule E2E', () => { 'test1@example.com', '', '', + '', + '', + [], 1, 1, AuthType.DEVICE, @@ -55,6 +58,9 @@ describe('UserModule E2E', () => { 'test2@example.com', '', '', + '', + '', + [], 1, 1, AuthType.DEVICE, @@ -74,6 +80,9 @@ describe('UserModule E2E', () => { 'test4@example.com', '23e21e', '23dwe', + 'Engineering', + 'Computer Science', + ['Nature'], 1, 1, AuthType.DEVICE, @@ -85,6 +94,9 @@ describe('UserModule E2E', () => { 'test4@example.com', 'wefwef', 'wef324f', + 'Engineering', + 'Computer Science', + ['Food'], 1, 1, AuthType.DEVICE, diff --git a/server/src/user/user.gateway.ts b/server/src/user/user.gateway.ts index b1292c2b..00f01942 100644 --- a/server/src/user/user.gateway.ts +++ b/server/src/user/user.gateway.ts @@ -20,9 +20,6 @@ import { RequestFavoriteEventDataDto, SetAuthToDeviceDto, SetAuthToOAuthDto, - SetGraduationYearDto, - SetMajorDto, - SetUsernameDto, UpdateUserDataDto, UserDto, BanUserDto, @@ -76,7 +73,12 @@ export class UserGateway { @CallingUser() user: User, @MessageBody() data: RequestAllUserDataDto, ) { - const users = await this.userService.getAllUserData(); + let users: User[] = []; + if (!user.administrator) { + users = [user]; + } else { + users = await this.userService.getAllUserData(); + } await Promise.all( users.map( @@ -103,7 +105,7 @@ export class UserGateway { ); } } else { - await this.userService.emitUpdateUserData(user, false, false); + await this.userService.emitUpdateUserData(user, false, false, user); } } diff --git a/server/src/user/user.service.ts b/server/src/user/user.service.ts index 4c66608c..d88abcbd 100644 --- a/server/src/user/user.service.ts +++ b/server/src/user/user.service.ts @@ -21,6 +21,7 @@ import { AppAbility, CaslAbilityFactory } from '../casl/casl-ability.factory'; import { Action } from '../casl/action.enum'; import { subject } from '@casl/ability'; import { accessibleBy } from '@casl/prisma'; +import { join } from 'path'; @Injectable() export class UserService { @@ -51,25 +52,24 @@ export class UserService { /** Registers a user using a certain authentication scheme */ async register( email: string, - username: string, + username: string | undefined, year: string, + college: string, + major: string, + interests: Array, lat: number, long: number, authType: AuthType, authToken: string, enrollmentType: EnrollmentType, ) { - if (username == null && authType == AuthType.GOOGLE) { - username = email?.split('@')[0]; - } else if (authType == AuthType.DEVICE) { + if (authType === AuthType.DEVICE) { const count = await this.prisma.user.count(); - username = 'guest' + count; + username = 'guest' + (count + 921); } const defOrg = await this.orgService.getDefaultOrganization( - authType == AuthType.GOOGLE - ? OrganizationSpecialUsage.CORNELL_LOGIN - : OrganizationSpecialUsage.DEVICE_LOGIN, + OrganizationSpecialUsage.DEVICE_LOGIN, ); const group: Group = await this.groupsService.createFromEvent( @@ -82,8 +82,11 @@ export class UserService { group: { connect: { id: group.id } }, hostOf: { connect: { id: group.id } }, memberOf: { connect: { id: defOrg.id } }, - username, + username: username ?? email?.split('@')[0], year, + college, + major, + interests, email, authToken, enrollmentType, @@ -100,6 +103,15 @@ export class UserService { await this.eventsService.createDefaultEventTracker(user, lat, long); console.log(`User ${user.id} created with username ${username}!`); await this.log.logEvent(SessionLogEvent.CREATE_USER, user.id, user.id); + + if (authType === AuthType.GOOGLE) { + const allOrg = await this.orgService.getDefaultOrganization( + OrganizationSpecialUsage.CORNELL_LOGIN, + ); + + await this.orgService.joinOrganization(user, allOrg.accessCode); + } + return user; } @@ -159,7 +171,7 @@ export class UserService { } /** - * Update a User's username, email, or year. + * Update a User's username, email, college, major, or year. * @param user User requiring an update. * @returns The new user after the update is made */ @@ -187,6 +199,8 @@ export class UserService { { username: username, email: user.email, + college: user.college, + major: user.major, year: user.year, }, 'User', @@ -217,6 +231,8 @@ export class UserService { username: joinedUser.username, enrollmentType: joinedUser.enrollmentType, email: joinedUser.email, + college: joinedUser.college, + major: joinedUser.major, year: joinedUser.year, score: joinedUser.score, groupId: joinedUser.group.friendlyId, @@ -255,7 +271,7 @@ export class UserService { await this.clientService.sendProtected( 'updateUserData', - target?.id ?? user.id, + target ?? user.id, dto, { id: user.id, diff --git a/server/start.sh b/server/start.sh index 69b32124..e6ce7257 100644 --- a/server/start.sh +++ b/server/start.sh @@ -12,5 +12,5 @@ elif [ "$TESTING_E2E" = "true" ]; then elif [ "$DEVELOPMENT" = "false" ]; then npm run start:prod; else - npm run start; + npm run start:debug; fi \ No newline at end of file