From 5429ddb67ec5d58d549567b036acdb054f6b619a Mon Sep 17 00:00:00 2001 From: Jake Gehrke <91503842+Gehrkej@users.noreply.github.com> Date: Fri, 12 Apr 2024 11:34:14 -0700 Subject: [PATCH] Error Handling + Helper Text (#94) * Added handling for non numbers and empty values * Added comments for future work * Frankreed/unify result screens (#96) * Result screens unified! * fixed the display bug in result * added title to styles, some redundancy removed from barChartScreen --------- Co-authored-by: Jake Gehrke * Planscreen player (#131) * plan hooked fully to database and finished by submitting drill * scroll down to refresh * change naming to be more clear and requre drill id and assigned_time * add assigned_data to user on user creation * fix key issue, lighter green, squircle, spacing of description * styling * updated prettier and github action settings to be more strict * code cleanup * ran pretty * attemptRefId into attemptId --------- Co-authored-by: Frankreed * added hard coded helper text * helperText is now pulled from database * Added handling for non numbers and empty values * Added comments for future work * added hard coded helper text * helperText is now pulled from database * added error handling for submit drill button * adjusted uploadAttempt function after rebase * updated SID to be 1-20 * added error handling for submit drill button * adjusted uploadAttempt function after rebase * updated SID to be 1-20 * Inital changes requested from frank in PR * Converted error banners to error dialogs * Frankreed/Bar chart is 60fps consistent now (#129) * Database updated and codebase uses title instead of drillType * Removed title and added prettyDrillType and subType * Added putting to database and updated codebase * Rebased with layout and prettiier ran * leaderboard is more resilient to incorrect data * style changes * barChart performance vastly improved. Frame rate is now pegged at 60 (for ios). Still crashes on Android. * this doesn't improve anything measurable, it just makes sense in my head * Paginate barChart * very minor tick change/add back * barChart now displays 0 as the minimum instead of the lowest value (if the lowest value is lower than 0, display that instead). Illegal attempts shall be terminated --------- Co-authored-by: hannacol * Minor cosmetic fixes (#138) * Database updated and codebase uses title instead of drillType * Removed title and added prettyDrillType and subType * Added putting to database and updated codebase * Rebased with layout and prettiier ran * made target an array for all drill types for consistency and fixed shotAccordion shotNum display * updated more code for target lists * Result screen works for putting drills * style changes * Ran prettier * Updated Headers to hold title better * prettier ran * ran prettier * added margin to the left and right of the segmented buttons * commented out a lot of prints, changed uid to a string instead of reference, sorted user in Teams based on role * Fixed shotAccordion's weird shot spacing --------- Co-authored-by: hannacol * Database updated and codebase uses title instead of drillType (#134) * Database updated and codebase uses title instead of drillType * Removed title and added prettyDrillType and subType * Added putting to database and updated codebase * Rebased with layout and prettiier ran * made target an array for all drill types for consistency and fixed shotAccordion shotNum display * Result screen works for putting drills * style changes * ran prettier * Made attemptShots use an object instead of an array for targets and componentized header * Fixed drill list styling and ran prettier after rebase --------- Co-authored-by: Frankreed * Reworking Drill Screens to be Componentized (#140) * DrillList has been componentized for main drill list and profile view to be sectioned off * Moved glyphs to be in section header instead of drill card * Drlll Description componentized for description screen and description modal and deleted image carousel * ran yarn pretty * slight optimization to getUnique * Removed prettyDrillType and modified drillType --------- Co-authored-by: Frankreed * ErrorDialog Component Created and Styled * Added styling to Leave Dialog * Swapped order of action buttons in leave drill dialog * Created submitVisable varible * Added handling for non numbers and empty values * Added comments for future work * added hard coded helper text * helperText is now pulled from database * added error handling for submit drill button * adjusted uploadAttempt function after rebase * updated SID to be 1-20 * Added handling for non numbers and empty values * added error handling for submit drill button * Inital changes requested from frank in PR * Converted error banners to error dialogs * ErrorDialog Component Created and Styled * Added styling to Leave Dialog * Swapped order of action buttons in leave drill dialog * Created submitVisable varible * added header import back * fix weird stuff with rebase * more weird rebase things * Restart Drill functionality working (#136) * fixed duplicate sid * All dialogs using DialogComponent * Added handling for non numbers and empty values * Added comments for future work * added hard coded helper text * helperText is now pulled from database * added error handling for submit drill button * adjusted uploadAttempt function after rebase * updated SID to be 1-20 * Added handling for non numbers and empty values * added error handling for submit drill button * Inital changes requested from frank in PR * Converted error banners to error dialogs * ErrorDialog Component Created and Styled * Added styling to Leave Dialog * Swapped order of action buttons in leave drill dialog * Created submitVisable varible * added header import back * Added handling for non numbers and empty values * Added comments for future work * adjusted uploadAttempt function after rebase * Added handling for non numbers and empty values * ErrorDialog Component Created and Styled * All dialogs using DialogComponent * Fixed uploadAttempt header comment * changed == to ===, and added one missing import, remove unused imports, and fixed calculateProxHole cuz I was stupid... * fixed faulty getUnique --------- Co-authored-by: Frank Nguyen <41023671+FrankreedX@users.noreply.github.com> Co-authored-by: ajpert <84763013+ajpert@users.noreply.github.com> Co-authored-by: Frankreed Co-authored-by: hannacol Co-authored-by: hannacol <91215417+hannacol@users.noreply.github.com> --- Utility.js | 4 +- app/content/profile/index.js | 26 +-- app/content/team/users/[user]/index.js | 2 +- app/segments/drill/[id]/submission/index.js | 5 +- app/segments/drill/[id]/submission/input.js | 184 +++++++++++--------- components/dialog.js | 58 ++++++ components/input/drillInput.js | 10 +- 7 files changed, 188 insertions(+), 101 deletions(-) create mode 100644 components/dialog.js diff --git a/Utility.js b/Utility.js index 8ce2ad81..860dd2ef 100644 --- a/Utility.js +++ b/Utility.js @@ -34,11 +34,11 @@ export function numTrunc(value, pad = false) { } } -export function getUnique(array, field, drills) { +export function getUnique(array, drills) { const unique = []; drills.forEach((drillInfo) => { const idx = array.findIndex((item) => item["did"] === drillInfo["did"]); - if (idx >= 0) unique.push(drills[idx]); + if (idx >= 0) unique.push(drillInfo); }); return unique; } diff --git a/app/content/profile/index.js b/app/content/profile/index.js index b1fbebc9..f8bb55dc 100644 --- a/app/content/profile/index.js +++ b/app/content/profile/index.js @@ -21,17 +21,10 @@ import { TouchableOpacity, View, } from "react-native"; -import { - Appbar, - Button, - Dialog, - PaperProvider, - Paragraph, - Portal, - Snackbar, -} from "react-native-paper"; +import { Appbar, PaperProvider, Snackbar } from "react-native-paper"; import { SafeAreaView } from "react-native-safe-area-context"; import { getUnique } from "~/Utility"; +import DialogComponent from "~/components/dialog"; import DrillList from "~/components/drillList"; import ErrorComponent from "~/components/errorComponent"; import Loading from "~/components/loading"; @@ -207,7 +200,7 @@ function Index(props) { } }; - const uniqueDrills = getUnique(attempts, "did", Object.values(drillInfo)); + const uniqueDrills = getUnique(attempts, Object.values(drillInfo)); return ( @@ -219,7 +212,7 @@ function Index(props) { {snackbarMessage} - + {/* setDialogVisible(false)} @@ -232,7 +225,16 @@ function Index(props) { - + */} + + setDialogVisible(false)} + buttons={["OK"]} + buttonsFunctions={[() => setDialogVisible(false)]} + /> diff --git a/app/content/team/users/[user]/index.js b/app/content/team/users/[user]/index.js index 45a106a9..10476bb8 100644 --- a/app/content/team/users/[user]/index.js +++ b/app/content/team/users/[user]/index.js @@ -42,7 +42,7 @@ function Index(props) { ); } - const uniqueDrills = getUnique(attempts, "did", Object.values(drillInfo)); + const uniqueDrills = getUnique(attempts, Object.values(drillInfo)); return ( diff --git a/app/segments/drill/[id]/submission/index.js b/app/segments/drill/[id]/submission/index.js index 37b8136d..3a4e07df 100644 --- a/app/segments/drill/[id]/submission/index.js +++ b/app/segments/drill/[id]/submission/index.js @@ -6,10 +6,11 @@ import Loading from "~/components/loading"; import Input from "./input"; import Result from "./result"; +import ErrorComponent from "~/components/errorComponent"; import { useDrillInfo } from "~/hooks/useDrillInfo"; export default function Index() { - const { id, assignedTime } = useLocalSearchParams(); + const { id } = useLocalSearchParams(); const [outputData, setOutputData] = useState([]); const [toggleResult, setToggleResult] = useState(false); @@ -25,7 +26,7 @@ export default function Index() { if (drillInfoError) return ; const display = () => { - if (toggleResult == true) { + if (toggleResult) { return ( { + const updatedAssignedData = assignedData.map((assignment) => { if ( assignment.assignedTime === assignedTime && assignment.drillId === drillId @@ -86,6 +77,7 @@ async function completeAssigned( getDocument(); } +//A function to upload the outputData to the "attempts" collection async function uploadAttempt( outputData, userId, @@ -110,6 +102,9 @@ async function uploadAttempt( await setDoc(newAttemptRef, uploadData) .then(() => { console.log("Document successfully uploaded!"); + //TODO: Call function to check for leaderboard update + + //Check if drill was assigned if (assignedTime) { completeAssigned( userId, @@ -130,6 +125,8 @@ async function uploadAttempt( } } +//TODO: Create a function to check leaderboard and update if needed + /*************************************** * AttemptShots Generation ***************************************/ @@ -147,7 +144,6 @@ function getShotInfo(drillInfo) { break; default: console.log("Shots not found"); - shots = []; break; } @@ -218,7 +214,7 @@ function fillPuttTargets(drillInfo) { //Helper funciton for createOutputData to calculate the Carry Difference function calculateProxHole(target, carry, sideLanding) { let carryDiff = calculateCarryDiff(target, carry); - return Math.sqrt(2 * Math.pow(carryDiff * 3, 2)); + return Math.sqrt(Math.pow(carryDiff * 3, 2) + Math.pow(sideLanding, 2)); } //Helper funciton for createOutputData to calculate the Carry Difference function calculateCarryDiff(target, carry) { @@ -336,7 +332,7 @@ function createOutputData(drillInfo, inputValues, attemptShots, uid, did) { } //add the sid to the shot - shot.sid = j; + shot.sid = j + 1; //push the shot into the array outputShotData.push(shot); @@ -395,6 +391,16 @@ function createOutputData(drillInfo, inputValues, attemptShots, uid, did) { return outputData; } +//A function to validate inputs are not empty +function checkEmptyInputs(inputs) { + return Object.values(inputs).some((value) => value === ""); +} + +//A function to validate inputs are all numbers +function validateInputs(inputs) { + return Object.values(inputs).some((input) => isNaN(input)); +} + export default function Input({ drillInfo, setToggleResult, setOutputData }) { //Helper varibles const { id, assignedTime } = useLocalSearchParams(); @@ -431,50 +437,36 @@ export default function Input({ drillInfo, setToggleResult, setOutputData }) { const descriptionModalRef = useRef(null); /***** Leave drill Dialog Stuff *****/ - const [leaveDialogVisible, setLeaveDialogVisible] = useState(false); const hideLeaveDialog = () => setLeaveDialogVisible(false); - /***** Empty Input Banner Stuff *****/ + /***** Empty Input dialog Stuff *****/ + const [emptyDialogVisible, setEmptyDialogVisible] = useState(false); + const hideEmptyDialog = () => setEmptyDialogVisible(false); - const [emptyInputBannerVisible, setEmptyInputBannerVisible] = useState(false); + /***** Invalid Input dialog Stuff *****/ + const [invalidDialogVisible, setInvalidDialogVisible] = useState(false); + const hideInvalidDialog = () => setInvalidDialogVisible(false); //useEffectHook to set the attempts shot requirements useEffect(() => { setattemptShots(getShotInfo(drillInfo)); }, []); + //Varible to store if Submit button is active + const submitVisible = + currentShot === drillInfo.reps - 1 && displayedShot === drillInfo.reps - 1; + //Changes the button depending on the current shot and shot index const buttonDisplayHandler = () => { //Logic to display "Submit Drill" - if ( - currentShot == drillInfo.reps - 1 && - displayedShot == drillInfo.reps - 1 - ) { + if (submitVisible) { return ( @@ -503,7 +495,7 @@ export default function Input({ drillInfo, setToggleResult, setOutputData }) { style={styles.button} labelStyle={styles.buttonText} mode="contained-tonal" - onPress={handleNextShotButtonClick} + onPress={handleButtonClick} > Next Shot @@ -523,14 +515,42 @@ export default function Input({ drillInfo, setToggleResult, setOutputData }) { }; //Function to handle "Next shot" button click - const handleNextShotButtonClick = () => { + const handleButtonClick = () => { //Check if all inputs have been filled in - if (Object.keys(inputValues[displayedShot]).length === numInputs) { - setEmptyInputBannerVisible(false); + if ( + Object.keys(inputValues[displayedShot]).length !== numInputs || + checkEmptyInputs(inputValues[displayedShot]) + ) { + setEmptyDialogVisible(true); + } + //check inputs are all numbers + else if (validateInputs(inputValues[displayedShot])) { + setInvalidDialogVisible(true); + } + //check for submit button + else if (submitVisible) { + let outputData = createOutputData( + inputValues, + attemptShots, + currentUserId, + did, + drillInfo.outputs, + drillInfo.aggOutputs, + ); + + setOutputData(outputData); + uploadAttempt( + outputData, + currentUserId, + currentTeamId, + assignedTime, + id, + queryClient, + ); + setToggleResult(true); + } else { setDisplayedShot(displayedShot + 1); setCurrentShot(currentShot + 1); - } else { - setEmptyInputBannerVisible(true); } }; @@ -564,19 +584,6 @@ export default function Input({ drillInfo, setToggleResult, setOutputData }) { color={"#F24E1E"} /> - {/* Empty Input Banner */} - - setEmptyInputBannerVisible(false), - }, - ]} - > - Error! All input fields must be filled! - {/* Shot Number / Total shots */} @@ -608,6 +615,7 @@ export default function Input({ drillInfo, setToggleResult, setOutputData }) { key={id} icon={getIconByKey(item.id)} prompt={item.prompt} + helperText={item.helperText} distanceMeasure={item.distanceMeasure} inputValue={inputValues[displayedShot]?.[item.id] || ""} onInputChange={(newText) => { @@ -664,28 +672,40 @@ export default function Input({ drillInfo, setToggleResult, setOutputData }) { {/* Leave Drill Dialog */} - - - Alert - - All inputs will be lost. - - - - - - - + { + hideLeaveDialog; + navigation.goBack(); + }, + ]} + /> + + {/* Error Dialog: Empty Input*/} + + + {/* Error Dialog: Invalid Input*/} + {/* Navigation */} diff --git a/components/dialog.js b/components/dialog.js new file mode 100644 index 00000000..fea8d445 --- /dev/null +++ b/components/dialog.js @@ -0,0 +1,58 @@ +import { Button, Dialog, Portal, Text } from "react-native-paper"; + +/** + * PROPS + * title - title to be displayed on the dialog + * content - main text to be displayed on the dialog + * visible - from useState Hook for this Dialog + * onHide - function to set useState Hook to false + * buttons - an array of strings for the buttons to be displayed + * buttonsFunctions - an array of functions for the buttons to be displayed + */ +export default function DialogComponent({ + title, + content, + visible, + onHide, + buttons, + buttonsFunctions, +}) { + const Buttons = buttons.map((item, index) => { + let style; + let labelStyle; + if (index === 0) { + style = {}; + labelStyle = { color: "#F24E1E" }; + } else { + style = { backgroundColor: "#F24E1E" }; + labelStyle = { color: "white" }; + } + + return ( + + ); + }); + + return ( + + + {title} + + {content} + + {Buttons} + + + ); +} diff --git a/components/input/drillInput.js b/components/input/drillInput.js index dfddc534..4bc1433f 100644 --- a/components/input/drillInput.js +++ b/components/input/drillInput.js @@ -4,6 +4,7 @@ import { Icon, Text, TextInput } from "react-native-paper"; export default function DrillInput({ icon, prompt, + helperText, distanceMeasure, inputValue, onInputChange, @@ -29,6 +30,7 @@ export default function DrillInput({ /> {distanceMeasure} + {helperText} ); } @@ -43,12 +45,16 @@ const styles = StyleSheet.create({ }, description: { fontSize: 20, - fontWeight: "bold", //temporary until I get the fonts to work + fontWeight: "bold", marginBottom: 10, }, distance: { fontSize: 40, - fontWeight: "200", //temporary until I get the fonts to work + fontWeight: "200", marginLeft: 10, }, + helper: { + fontSize: 12, + fontWeight: "200", + }, });