Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Frankreed/Leaderboard precalculations now stored on Firebase! #92

Merged
merged 4 commits into from
Apr 4, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/content/drill/[id]/attempts/[attempt].js
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While it isn't directly correlated to this PR, currently there is no way to go back when looking at drill results. I think a simple solution would be adding appbar with a back chevron

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm thinking of tackling this in a different PR because I want to merge the 2 result screen into 1 file.

Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ function Result() {
<Text style={styles.sectionTitle}>Drill Results</Text>

{Object.keys(drillInfo).length > 0 &&
drillInfo["aggOutputs"].map((output) => (
Object.keys(drillInfo["aggOutputs"]).map((output) => (
<View
style={{
flexDirection: "row",
Expand Down
146 changes: 102 additions & 44 deletions app/content/drill/[id]/leaderboard.js
Original file line number Diff line number Diff line change
@@ -1,20 +1,25 @@
import { Link, useLocalSearchParams, usePathname } from "expo-router";
import { useState } from "react";
import { useEffect, useState } from "react";
import { ScrollView, View } from "react-native";
import { Avatar, Icon, List, Text } from "react-native-paper";
import { numTrunc } from "~/Utility";
import ErrorComponent from "~/components/errorComponent";
import Loading from "~/components/loading";
import { currentAuthContext } from "~/context/Auth";
import { updateLeaderboard } from "~/hooks/updateLeaderboard";
import { useAttempts } from "~/hooks/useAttempts";
import { useDrillInfo } from "~/hooks/useDrillInfo";
import { useLeaderboard } from "~/hooks/useLeaderboard";
import { useUserInfo } from "~/hooks/useUserInfo";

export default function Leaderboard() {
const { currentTeamId } = currentAuthContext();
const drillId = useLocalSearchParams()["id"];
const currentPath = usePathname();
const [defaultMainOutputAttempt, setDefaultMainOutputAttempt] =
useState(true); //whether mainOutputAttempt is the default set on drills or has been changed by user
const [customMainOutputAttempt, setCustomMainOutputAttempt] = useState("did"); //What is the custom mainOutputAttempt in case defaultMainOutputAttempt is false
const [manualAttemptCalc, setManualAttemptCalc] = useState(false); //whether the attempt is manually calculated or grabbed from precalculated leaderboard

const {
data: userInfo,
Expand All @@ -28,71 +33,124 @@ export default function Leaderboard() {
error: drillError,
} = useDrillInfo(drillId);

const {
data: preCalcLeaderboard,
isLoading: leaderboardIsLoading,
error: leaderboardError,
} = useLeaderboard({ drillId });

useEffect(() => {
setManualAttemptCalc(
!drillIsLoading && // so that mainOutputAttempt is calculated
!leaderboardIsLoading && //leaderboard must've finished loading
(!preCalcLeaderboard || //and not exist
preCalcLeaderboard[Object.keys(preCalcLeaderboard)[0]][
mainOutputAttempt
] === undefined), //or exist but does not have the required field
);
}, [drillIsLoading, leaderboardIsLoading, preCalcLeaderboard]);

// console.log("enabled: ", manualAttempt);

const {
data: attempts,
isLoading: attemptIsLoading,
error: attemptError,
} = useAttempts({ drillId });

//console.log("userInfo: ", userInfo);
//console.log("drillInfo: ", drillInfo);
} = useAttempts({
drillId,
enabled: manualAttemptCalc,
});

if (userIsLoading || drillIsLoading || attemptIsLoading) {
if (
userIsLoading ||
drillIsLoading ||
attemptIsLoading ||
leaderboardIsLoading
) {
return <Loading />;
}

if (userError || drillError || attemptError) {
return <ErrorComponent message={[userError, drillError, attemptError]} />;
if (userError || drillError || attemptError || leaderboardError) {
return (
<ErrorComponent
message={[userError, drillError, attemptError, leaderboardError]}
/>
);
}

// console.log("attempts: ", attempts);

const mainOutputAttempt = defaultMainOutputAttempt
? drillInfo["mainOutputAttempt"]
: customMainOutputAttempt;

const drillLeaderboardAttempts = {};
for (const id in attempts) {
const entry = attempts[id];
// If this uid has not been seen before or the current score is higher, store it
if (
!drillLeaderboardAttempts[entry.uid] ||
drillLeaderboardAttempts[entry.uid][mainOutputAttempt] <
entry[mainOutputAttempt]
) {
drillLeaderboardAttempts[entry.uid] = entry;
const leaderboardAttempts = preCalcLeaderboard || {};
if (!preCalcLeaderboard && attempts) {
//just in case...
for (const id in attempts) {
const entry = attempts[id];

const lowerIsBetter =
drillInfo["aggOutputs"][mainOutputAttempt]["lowerIsBetter"];
// If this uid has not been seen before or the current score is higher, store it
if (
!leaderboardAttempts[entry.uid] ||
(lowerIsBetter &&
leaderboardAttempts[entry.uid][mainOutputAttempt]["value"] <
entry[mainOutputAttempt]) ||
(!lowerIsBetter &&
leaderboardAttempts[entry.uid][mainOutputAttempt]["value"] >
entry[mainOutputAttempt])
) {
leaderboardAttempts[entry.uid] = {
[mainOutputAttempt]: {
value: entry[mainOutputAttempt],
id: entry.id,
},
};
}
}

updateLeaderboard({
currentTeamId,
drillId,
value: leaderboardAttempts,
});
}

const orderedLeaderboard = Object.values(drillLeaderboardAttempts).sort(
(a, b) => a[mainOutputAttempt] - b[mainOutputAttempt],
);
console.log("drillLeaderboardAttempts: ", leaderboardAttempts);

// console.log(orderedLeaderboard[0]);
const orderedLeaderboard = Object.keys(leaderboardAttempts).sort(
//only sort the userId
(a, b) =>
leaderboardAttempts[a][mainOutputAttempt]["value"] -
leaderboardAttempts[b][mainOutputAttempt]["value"],
);

return (
<ScrollView>
<List.Section style={{ marginLeft: 20 }}>
{orderedLeaderboard.map((attempt) => (
<Link
key={attempt["uid"]}
href={{
pathname: `${currentPath}/attempts/${attempt["id"]}`,
}}
asChild
>
<List.Item
title={userInfo[attempt["uid"]]["name"]}
left={() => <Avatar.Text size={24} label="XD" />}
right={() => (
<View style={{ flexDirection: "row", alignItems: "center" }}>
<Text>{numTrunc(attempt[mainOutputAttempt])} ft</Text>
<Icon source="chevron-right" />
</View>
)}
/>
</Link>
))}
{orderedLeaderboard.map((userId) => {
const attempt = leaderboardAttempts[userId][mainOutputAttempt];
return (
<Link
key={userId}
href={{
pathname: `${currentPath}/attempts/${attempt["id"]}`,
}}
asChild
>
<List.Item
title={userInfo[userId]["name"]}
left={() => <Avatar.Text size={24} label="XD" />}
right={() => (
<View style={{ flexDirection: "row", alignItems: "center" }}>
<Text>{numTrunc(attempt["value"])} ft</Text>
<Icon source="chevron-right" />
</View>
)}
/>
</Link>
);
})}
</List.Section>
</ScrollView>
);
Expand Down
5 changes: 3 additions & 2 deletions app/segments/drill/[id]/submission/input.js
Original file line number Diff line number Diff line change
Expand Up @@ -243,8 +243,9 @@ function createOutputData(

const aggOutputs = Object.keys(aggOutputsObj);
//Generate the aggOutputs for output data
for (let i = 0; i < aggOutputs.length; i++) {
const aggOutput = aggOutputs[i];
const aggOutputsArr = Object.keys(aggOutputs);
for (let i = 0; i < aggOutputsArr.length; i++) {
const aggOutput = aggOutputsArr[i];

switch (aggOutput) {
case "carryDiffAverage":
Expand Down
24 changes: 24 additions & 0 deletions db_spec.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,23 @@
],
},
},
"best_attempts": {
//collection of did
"732489": {
//document of did, uid is the key
"2": {
//uid, document, contains a map item
"strokesGained": {
"value": "1237486", //the best value
"id": "567432", //attempt id that contains the "best" score for this field
},
"proxHole": {
"value": "1237486",
"id": "567432",
},
},
},
},
"drills": [
{
"did": "732489", // drillInfo id
Expand Down Expand Up @@ -162,6 +179,13 @@
"expectedPutts",
"strokesGained",
], //all data that will be stored in a shot
"aggOutputs": {
"strokesGained": { "lowerIsBetter": true },
"strokesGainedAverage": { "lowerIsBetter": true },
"carryDiffAverage": { "lowerIsBetter": false },
"sideLandingAverage": { "lowerIsBetter": false },
"proxHoleAverage": { "lowerIsBetter": false },
}, //all data that will be stored in an attempt
"mainOutputAttempt": "sideLandingAverage", // stat to be used for the barChart in statistics. This is the main data for attempt level data
"mainOutputShot": "sideLanding", // stat to be used for the big number in shotAccordion. This is the main data for shot level data
"reps": 20,
Expand Down
45 changes: 45 additions & 0 deletions hooks/updateLeaderboard.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { doc, setDoc, updateDoc } from "firebase/firestore";
import { db } from "~/firebaseConfig";
export const updateLeaderboard = async ({
currentTeamId,
drillId = null,
userId = null,
attemptId = null,
value = null,
}) => {
if (!currentTeamId || !drillId || !value) {
console.log("currentTeamId, drillId, or value not passed in");
return;
}
if (userId) {
//update a user's best after an attempt. Only pass in values to update, don't pass in everything...
if (!attemptId) {
console.log("attemptId or value not passed in");
return;
}
const leaderboardRef = doc(
db,
"teams",
currentTeamId,
"best_attempts",
drillId,
);

const changedObj = { userId: {} };
Object.keys(value).forEach((key) => {
changedObj[userId][key] = { value: value[key], id: attemptId };
});

return updateDoc(leaderboardRef, changedObj);
} else {
//update the leaderboard after a new field is added, or initialize the leaderboard with a certain field (only 1 field at a time as only 1 field is sorted at one time)
const leaderboardRef = doc(
db,
"teams",
currentTeamId,
"best_attempts",
drillId,
);
return setDoc(leaderboardRef, value, { merge: true });
}
};
3 changes: 3 additions & 0 deletions hooks/useAttempts.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,13 @@ export const useAttempts = ({
attemptId = null,
userId = null,
drillId = null,
enabled = true,
}) => {
const { currentTeamId } = currentAuthContext();
const { data, error, isLoading } = useQuery({
queryKey: ["attempts", currentTeamId, { attemptId, userId, drillId }],
queryFn: async () => {
console.log("fetching attempts");
if (attemptId) {
const querySnapshot = await getDoc(
doc(db, "teams", currentTeamId, "attempts", attemptId),
Expand All @@ -38,6 +40,7 @@ export const useAttempts = ({
return querySnapshot.docs.map((doc) => doc.data());
}
},
enabled,
});

return {
Expand Down
29 changes: 29 additions & 0 deletions hooks/useLeaderboard.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { useQuery } from "@tanstack/react-query";
import { doc, getDoc } from "firebase/firestore";
import { currentAuthContext } from "~/context/Auth";
import { db } from "~/firebaseConfig";

export const useLeaderboard = ({ drillId = null }) => {
const { currentTeamId } = currentAuthContext();
const { data, error, isLoading } = useQuery({
queryKey: ["best_attempts", currentTeamId, drillId],
queryFn: async () => {
// Fetch all drills info
const newLeaderboard = {};
const querySnapshot = await getDoc(
doc(db, "teams", currentTeamId, "best_attempts", drillId),
);
const data = querySnapshot.data();
if (data === undefined) {
return false;
}
return querySnapshot.data();
},
});

return {
data,
error,
isLoading,
};
};
Loading