diff --git a/app/src/FormHelper.js b/app/src/FormHelper.js index 6e810b92..58b4e4dd 100644 --- a/app/src/FormHelper.js +++ b/app/src/FormHelper.js @@ -5,6 +5,8 @@ class FormHelper { static LATEST_FORM = 'current-form'; static ASYNCSTORAGE_COMPETITION_KEY = 'current-competition'; static ASYNCSTORAGE_MATCHES_KEY = 'current-matches'; + // used to save progress on a current/wip scouting report + static ASYNCSTORAGE_CURRENT_REPORT_KEY = 'current-report'; static SCOUTING_STYLE = 'scoutingStyle'; static THEME = 'themePreference'; static OLED = 'oled'; diff --git a/app/src/components/NoteList.tsx b/app/src/components/NoteList.tsx index d4d75147..54639d0b 100644 --- a/app/src/components/NoteList.tsx +++ b/app/src/components/NoteList.tsx @@ -1,4 +1,5 @@ import { + Alert, FlatList, Modal, Pressable, @@ -11,6 +12,7 @@ import React, {useEffect, useState} from 'react'; import {useTheme} from '@react-navigation/native'; import {NoteStructureWithMatchNumber, OfflineNote} from '../database/Notes'; import Svg, {Path} from 'react-native-svg'; +import {EditNoteModal} from './modals/EditNoteModal'; export enum FilterType { // todo: allow note list to accept and display team # @@ -29,17 +31,32 @@ export const NoteList = ({ }) => { const {colors} = useTheme(); const [searchTerm, setSearchTerm] = useState(''); - const [filteredNotes, setFilteredNotes] = + const [notesCopy, setNotesCopy] = useState<(NoteStructureWithMatchNumber | OfflineNote)[]>(notes); + const [filteredNotes, setFilteredNotes] = useState< + { + note: NoteStructureWithMatchNumber | OfflineNote; + index: number; + }[] + >(notes.map((note, i) => ({note, index: i}))); const [filterBy, setFilterBy] = useState(FilterType.TEXT); const [filterModalVisible, setFilterModalVisible] = useState(false); + const [editModalVisible, setEditModalVisible] = useState(false); + const [currentNote, setCurrentNote] = + useState(null); + const [currentNoteIndex, setCurrentNoteIndex] = useState(-1); + useEffect(() => { + const mapped = notesCopy.map((note, i) => ({ + note, + index: i, + })); if (searchTerm === '') { - setFilteredNotes(notes); + setFilteredNotes(mapped); return; } - const filtered = notes.filter(note => { + const filtered = mapped.filter(({note}) => { if (filterBy === FilterType.MATCH_NUMBER) { return ( note.match_number?.toString().includes(searchTerm) || @@ -49,7 +66,7 @@ export const NoteList = ({ return note.content.toLowerCase().includes(searchTerm.toLowerCase()); }); setFilteredNotes(filtered); - }, [searchTerm]); + }, [searchTerm, filterBy, notesCopy]); const styles = StyleSheet.create({ container: { @@ -110,8 +127,8 @@ export const NoteList = ({ }}> @@ -134,7 +151,7 @@ export const NoteList = ({ {filteredNotes.length > 0 && ( ( + renderItem={({item: {note: item, index}}) => ( { + if (!('id' in item)) { + Alert.alert('You cannot edit an offline note!'); + return; + } + setCurrentNote(item); + setCurrentNoteIndex(index); + setEditModalVisible(true); }}> @@ -236,6 +262,25 @@ export const NoteList = ({ )} + {editModalVisible && currentNote && ( + { + setEditModalVisible(false); + setCurrentNote(null); + setCurrentNoteIndex(-1); + const newNotes = [...notesCopy]; + newNotes[currentNoteIndex] = note; + setNotesCopy(newNotes); + }} + onCancel={() => { + setEditModalVisible(false); + setCurrentNote(null); + setCurrentNoteIndex(-1); + }} + /> + )} ); }; diff --git a/app/src/components/form/Stepper.js b/app/src/components/form/Stepper.js index 92c8285a..75fe6bb4 100644 --- a/app/src/components/form/Stepper.js +++ b/app/src/components/form/Stepper.js @@ -3,6 +3,7 @@ import {View, StyleSheet, TouchableOpacity} from 'react-native'; import {Text} from 'react-native'; import Question from './Question'; import {useTheme} from '@react-navigation/native'; +import ReactNativeHapticFeedback from 'react-native-haptic-feedback'; function Stepper(props) { const {colors} = useTheme(); @@ -12,9 +13,17 @@ function Stepper(props) { props.onValueChange( props.value === '' ? 0 : Number.parseInt(props.value, 10) + 1, ); + ReactNativeHapticFeedback.trigger('impactMedium', { + enableVibrateFallback: true, + ignoreAndroidSystemSettings: false, + }); } else { if (props.value > 0) { props.onValueChange(props.value - 1); + ReactNativeHapticFeedback.trigger('impactSoft', { + enableVibrateFallback: true, + ignoreAndroidSystemSettings: false, + }); } } }; diff --git a/app/src/components/modals/EditNoteModal.tsx b/app/src/components/modals/EditNoteModal.tsx new file mode 100644 index 00000000..41917347 --- /dev/null +++ b/app/src/components/modals/EditNoteModal.tsx @@ -0,0 +1,152 @@ +import { + Modal, + Pressable, + StyleSheet, + Text, + TextInput, + View, +} from 'react-native'; +import {useTheme} from '@react-navigation/native'; +import {useState} from 'react'; +import {NoteStructureWithMatchNumber} from '../../database/Notes'; +import Svg, {Path} from 'react-native-svg'; +import {supabase} from '../../lib/supabase'; + +export const EditNoteModal = ({ + visible, + note, + onSave, + onCancel, +}: { + visible: boolean; + note: NoteStructureWithMatchNumber; + onSave: (note: NoteStructureWithMatchNumber) => void; + onCancel: () => void; +}) => { + const {colors} = useTheme(); + const [content, setContent] = useState(note.content); + + const saveNote = async () => { + await supabase.from('notes').update({content}).eq('id', note.id); + // await supabase.from('notes_edits').insert({ + // note_id: note.id, + // content: note.content, + // new_content: content, + // }); + onSave({...note, content}); + }; + + const styles = StyleSheet.create({ + container: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + }, + modal: { + backgroundColor: colors.card, + padding: 20, + borderRadius: 10, + width: '80%', + }, + backgroundCovering: { + backgroundColor: 'rgba(0, 0, 0, 0.5)', + position: 'absolute', + top: 0, + right: 0, + bottom: 0, + left: 0, + }, + input: { + backgroundColor: colors.background, + padding: 10, + borderRadius: 5, + marginVertical: 10, + height: '30%', + color: colors.text, + }, + button: { + backgroundColor: colors.primary, + padding: 10, + borderRadius: 5, + marginVertical: 10, + alignItems: 'center', + }, + disabledButton: { + backgroundColor: 'gray', + padding: 10, + borderRadius: 5, + marginVertical: 10, + alignItems: 'center', + }, + buttonText: { + color: colors.background, + }, + }); + + return ( + + + + + + + Edit Note + + + + + + + + + + Match: {note.match_number} + + + Team: {note.team_number} + + setContent(e.nativeEvent.text)} + /> + + Save + + + + + ); +}; diff --git a/app/src/screens/home-flow/components/NoteInputModal.tsx b/app/src/screens/home-flow/components/NoteInputModal.tsx index 45e190de..c6070525 100644 --- a/app/src/screens/home-flow/components/NoteInputModal.tsx +++ b/app/src/screens/home-flow/components/NoteInputModal.tsx @@ -83,7 +83,8 @@ export const NoteInputModal = ({ { + onChange={evt => { + const text = evt.nativeEvent.text; setLocalContent(text); setNoteContents({ ...noteContents, diff --git a/app/src/screens/scouting-flow/ScoutingFlow.js b/app/src/screens/scouting-flow/ScoutingFlow.js index 64ede75b..b55edf24 100644 --- a/app/src/screens/scouting-flow/ScoutingFlow.js +++ b/app/src/screens/scouting-flow/ScoutingFlow.js @@ -167,7 +167,18 @@ function ScoutingFlow({navigation, route, resetTimer}) { setFormId(comp.formId); setFormStructure(comp.form); setCompetition(comp); - initForm(comp.form); + const storedFormData = await AsyncStorage.getItem( + FormHelper.ASYNCSTORAGE_CURRENT_REPORT_KEY, + ); + console.log('storedFormData: ', storedFormData); + if (storedFormData != null) { + const data = JSON.parse(storedFormData); + setArrayData(data.arrayData); + setMatch(data.match); + setTeam(data.team); + } else { + initForm(comp.form); + } } else { setIsCompetitionHappening(false); } @@ -357,6 +368,22 @@ function ScoutingFlow({navigation, route, resetTimer}) { //console.log('dict: ', dict); }, [formStructure]); + useEffect(() => { + if (arrayData == null) { + return; + } + (async () => { + await AsyncStorage.setItem( + FormHelper.ASYNCSTORAGE_CURRENT_REPORT_KEY, + JSON.stringify({ + arrayData, + match, + team, + }), + ); + })(); + }, [arrayData]); + const styles = StyleSheet.create({ textInput: { borderColor: 'gray', diff --git a/app/src/screens/search-flow/SearchMain.tsx b/app/src/screens/search-flow/SearchMain.tsx index 55755b68..bbb615e8 100644 --- a/app/src/screens/search-flow/SearchMain.tsx +++ b/app/src/screens/search-flow/SearchMain.tsx @@ -9,7 +9,7 @@ import { Alert, } from 'react-native'; import React, {useCallback, useEffect, useState} from 'react'; -import {useTheme} from '@react-navigation/native'; +import {useNavigation, useTheme} from '@react-navigation/native'; import Svg, {Path} from 'react-native-svg'; import {ScoutReportReturnData} from '../../database/ScoutReports'; @@ -28,10 +28,15 @@ import {NoteList} from '../../components/NoteList'; interface Props { setChosenTeam: (team: SimpleTeam) => void; + route: { + params: { + searchEnabled: boolean; + }; + }; navigation: any; } -const SearchMain: React.FC = ({navigation}) => { +const SearchMain: React.FC = ({route, navigation}) => { const {colors} = useTheme(); const [listOfTeams, setListOfTeams] = useState([]); @@ -134,6 +139,16 @@ const SearchMain: React.FC = ({navigation}) => { }); }, [fetchData, navigation]); + // why is this not in a useEffect? we want to check searchEnabled every time the route changes + // having it in a useEffect would only check it once on initial render + if (route.params && route.params.searchEnabled && !fetchingData) { + navigation.navigate('SearchModal', { + teams: listOfTeams, + reportsByMatch: reportsByMatch, + competitionId: competitionId, + }); + } + const navigateIntoReport = (report: ScoutReportReturnData) => { setScoutViewerVisible(true); setCurrentReport(report); diff --git a/app/src/screens/search-flow/SearchScreen.tsx b/app/src/screens/search-flow/SearchScreen.tsx index aa905105..e9c8ab19 100644 --- a/app/src/screens/search-flow/SearchScreen.tsx +++ b/app/src/screens/search-flow/SearchScreen.tsx @@ -5,14 +5,39 @@ import TeamViewer from './TeamViewer'; import {SimpleTeam} from '../../lib/TBAUtils'; import {createStackNavigator} from '@react-navigation/stack'; import ReportsForTeam from './ReportsForTeam'; -import {useTheme} from '@react-navigation/native'; +import {useNavigation, useTheme} from '@react-navigation/native'; import ScoutViewer from '../../components/modals/ScoutViewer'; -import SearchModal from "./SearchModal"; +import SearchModal from './SearchModal'; +import type {BottomTabNavigationProp} from '@react-navigation/bottom-tabs'; const Stack = createStackNavigator(); -function SearchScreen() { + +function SearchScreen({ + navigation: rootNavigation, +}: { + navigation: BottomTabNavigationProp<{}>; +}) { const [team, setChosenTeam] = useState(); const {colors} = useTheme(); + const navigation = useNavigation(); + + useEffect(() => { + const unsubscribe = rootNavigation.addListener('tabPress', e => { + console.log( + 'tab press', + navigation.isFocused(), + rootNavigation.isFocused(), + ); + if (navigation.isFocused()) { + e.preventDefault(); + navigation.navigate('Main Search', { + searchEnabled: true, + }); + } + }); + + return unsubscribe; + }, [rootNavigation]); // if (team === null || team === undefined) { // return ;