Skip to content

Commit

Permalink
Dont allow editing stat value without stat type chosen and dont allow…
Browse files Browse the repository at this point in the history
… setting stat type with any value(s) entered, when deleting stat type show info about entries that used it, fix bug with improving PBs + some UI, UX and DX improvements
  • Loading branch information
dmint789 committed Dec 31, 2022
1 parent e153419 commit 1d4b59d
Show file tree
Hide file tree
Showing 8 changed files with 146 additions and 109 deletions.
4 changes: 2 additions & 2 deletions android/app/src/main/assets/index.android.bundle

Large diffs are not rendered by default.

21 changes: 16 additions & 5 deletions app/components/StatTypeModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { useSelector, useDispatch } from 'react-redux';
import { RootState, AppDispatch } from '../redux/store';
import { reorderStatTypes, deleteStatType } from '../redux/mainSlice';
import GS from '../shared/GlobalStyles';
import { formatIDate } from '../shared/GlobalFunctions';
import { IStatType } from '../shared/DataStructure';

import IconButton from './IconButton';
Expand All @@ -17,7 +18,7 @@ const ChooseStatModal: React.FC<{
onEditStatType: (statType: IStatType) => void;
}> = ({ modalOpen, setStatModalOpen, filteredStatTypes, selectStatType, onAddStatType, onEditStatType }) => {
const dispatch = useDispatch<AppDispatch>();
const { statTypes } = useSelector((state: RootState) => state.main);
const { statTypes, entries } = useSelector((state: RootState) => state.main);

const [reordering, setReordering] = useState<boolean>(false);

Expand All @@ -28,17 +29,27 @@ const ChooseStatModal: React.FC<{
};

const onDeleteStatType = (statType: IStatType) => {
Alert.alert('Confirmation', `Are you sure you want to delete the stat type ${statType.name}?`, [
let message = `Are you sure you want to delete the stat type ${statType.name}?`;
const orphanEntries = entries.filter((el) => !!el.stats.find((st) => st.type === statType.id));

if (orphanEntries.length > 0) {
message += ` You have ${orphanEntries.length} entries that use it! They were made on these dates: `;
const iterations = Math.min(3, orphanEntries.length);
for (let i = 0; i < iterations; i++) {
message += formatIDate(orphanEntries[i].date);
message += i !== iterations - 1 ? ', ' : ' ...';
}
}

Alert.alert(orphanEntries.length === 0 ? 'Confirmation' : 'WARNING!', message, [
{ text: 'Cancel' },
{
text: 'Ok',
onPress: () => {
dispatch(deleteStatType(statType.id));

// -1, because it won't be updated until the next tick
if (statTypes.length - 1 === 0) {
setStatModalOpen(false);
}
if (statTypes.length - 1 === 0) setStatModalOpen(false);
},
},
]);
Expand Down
115 changes: 64 additions & 51 deletions app/redux/mainSlice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
StatTypeVariant,
} from '../shared/DataStructure';

const verbose = true;
const verbose = false;

////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////
Expand Down Expand Up @@ -127,70 +127,83 @@ const updatePBs = (state: any, entry: IEntry, mode: 'add' | 'edit' | 'delete') =
// Update PB if it could have gotten worse
// Skip checking PB if it was already updated and the stat type only has single values
if (mode !== 'add' && (!pbUpdated || statType.multipleValues)) {
let isPrevPB = false;
const tempStatType: IStatType = {
...statType,
pbs: {
allTime: {
entryId: { ...statType.pbs.allTime.entryId },
result: { ...statType.pbs.allTime.result },
if (statType.pbs) {
let isPrevPB = false;
const tempStatType: IStatType = {
...statType,
pbs: {
allTime: {
entryId: { ...statType.pbs.allTime.entryId },
result: { ...statType.pbs.allTime.result },
},
},
},
};
};

if (!statType.multipleValues && statType.pbs.allTime.entryId === entry.id) {
isPrevPB = true;
tempStatType.pbs = null;
} else if (statType.multipleValues) {
['best', 'avg', 'sum'].forEach((key) => {
if (statType.pbs.allTime.entryId[key] === entry.id) {
isPrevPB = true;
tempStatType.pbs.allTime.entryId[key] = null;
tempStatType.pbs.allTime.result[key] = null;
}
});
}

if (!statType.multipleValues && statType.pbs.allTime.entryId === entry.id) {
isPrevPB = true;
tempStatType.pbs = null;
} else if (statType.multipleValues) {
['best', 'avg', 'sum'].forEach((key) => {
if (statType.pbs.allTime.entryId[key] === entry.id) {
isPrevPB = true;
tempStatType.pbs.allTime.entryId[key] = null;
tempStatType.pbs.allTime.result[key] = null;
}
});
}
if (isPrevPB) {
checkPBFromScratch(state, tempStatType);

if (isPrevPB) {
checkPBFromScratch(state, tempStatType);

if (!tempStatType.multipleValues) {
// If this is null, that means no entries are left with this stat type
if (tempStatType.pbs === null) {
pbUpdated = true;
delete statType.pbs;
} else if (
tempStatType.pbs.allTime.result !== statType.pbs.allTime.result ||
tempStatType.pbs.allTime.entryId !== statType.pbs.allTime.entryId
) {
pbUpdated = true;
statType.pbs = tempStatType.pbs;
}
} else {
// If this is null, that means no entries are left with this stat type.
// It doesn't have to be best, because avg and sum would also be null if best is null.
if (tempStatType.pbs.allTime.entryId['best'] === null) {
pbUpdated = true;
delete statType.pbs;
} else {
if (
!!['best', 'avg', 'sum'].find(
(key) =>
tempStatType.pbs.allTime.entryId[key] !== statType.pbs.allTime.entryId[key] ||
tempStatType.pbs.allTime.result[key] !== statType.pbs.allTime.result[key],
)
if (!tempStatType.multipleValues) {
// If this is null, that means no entries are left with this stat type
if (tempStatType.pbs === null) {
pbUpdated = true;
delete statType.pbs;
} else if (
tempStatType.pbs.allTime.result !== statType.pbs.allTime.result ||
tempStatType.pbs.allTime.entryId !== statType.pbs.allTime.entryId
) {
pbUpdated = true;
statType.pbs = tempStatType.pbs;
}
} else {
// If this is null, that means no entries are left with this stat type.
// It doesn't have to be best, because avg and sum would also be null if best is null.
if (tempStatType.pbs.allTime.entryId['best'] === null) {
pbUpdated = true;
delete statType.pbs;
} else {
if (
!!['best', 'avg', 'sum'].find(
(key) =>
tempStatType.pbs.allTime.entryId[key] !== statType.pbs.allTime.entryId[key] ||
tempStatType.pbs.allTime.result[key] !== statType.pbs.allTime.result[key],
)
) {
pbUpdated = true;
statType.pbs = tempStatType.pbs;
}
}
}
}
} else {
console.error('Stat type has no PB, which cannot be the case!');
}
}

PBsUpdated = pbUpdated || PBsUpdated;

PBsUpdated = pbUpdated || PBsUpdated;
if (verbose) {
if (pbUpdated) {
console.log('PB updated to: ', JSON.stringify(statType.pbs, null, 2));
} else {
console.log('PB not updated');
}
}
}
}
console.log(PBsUpdated);

if (PBsUpdated) SM.setData(state.statCategory.id, 'statTypes', state.statTypes);
};
Expand Down
41 changes: 20 additions & 21 deletions app/screens/AddEditEntry.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ const AddEditEntry = ({ navigation, route }) => {
// Stat choice from the list of filtered stat types
const [selectedStatType, setSelectedStatType] = useState<IStatType>(statTypes[0] || null);
// The type here is different from the type of values in IStat
const [statValues, setStatValues] = useState<Array<string | number>>(['']);
const [statValues, setStatValues] = useState<string[]>(['']);
const [comment, setComment] = useState<string>('');
const [date, setDate] = useState<Date>(new Date());
const [textDate, setTextDate] = useState<string>('');
Expand Down Expand Up @@ -87,7 +87,7 @@ const AddEditEntry = ({ navigation, route }) => {
return false;
} else if (
selectedStatType.variant === StatTypeVariant.NUMBER &&
statValues.find((el: string | number) => isNaN(Number(el))) !== undefined
!!statValues.find((el: string) => isNaN(Number(el)))
) {
if (showAlerts) {
const error = selectedStatType.multipleValues
Expand All @@ -100,10 +100,8 @@ const AddEditEntry = ({ navigation, route }) => {
};

const updateStatValues = (index: number, value: string) => {
setStatValues((prevStatValues) => {
const newStatValues = prevStatValues.map((prevValue: string | number, i) =>
i === index ? value : prevValue,
);
setStatValues((prevStatValues: string[]) => {
const newStatValues = prevStatValues.map((prevValue, i) => (i === index ? value : prevValue));

// Add extra value input, if no empty ones are left and the stat type allows multiple values
if (selectedStatType?.multipleValues && newStatValues.findIndex((val) => val === '') === -1) {
Expand All @@ -116,20 +114,18 @@ const AddEditEntry = ({ navigation, route }) => {

// Assumes the new stat is valid
const getNewStats = (prevStats = stats): IStat[] => {
let formatted;
let formatted = statValues.filter((val) => val !== '') as string[] | number[];
const mvs = {} as IMultiValueStat;

if (selectedStatType.variant === StatTypeVariant.NUMBER) {
formatted = statValues.filter((val) => val !== '').map((val) => Number(val)) as number[];
formatted = formatted.map((val) => Number(val));

if (selectedStatType.multipleValues) {
mvs.sum = formatted.reduce((acc, val) => acc + val, 0);
mvs.low = Math.min(...formatted);
mvs.high = Math.max(...formatted);
mvs.avg = Math.round((mvs.sum / formatted.length + Number.EPSILON) * 100) / 100;
}
} else {
formatted = statValues.filter((val) => val !== '').map((val) => String(val)) as string[];
}

const newStat: IStat = {
Expand Down Expand Up @@ -161,7 +157,7 @@ const AddEditEntry = ({ navigation, route }) => {
const newValues = statTypes.find((el) => el.id === stat.type)?.multipleValues
? [...stat.values, '']
: stat.values;
setStatValues(newValues);
setStatValues(newValues.map((el) => String(el)));
selectStatType(stat.type);
}
};
Expand Down Expand Up @@ -250,28 +246,31 @@ const AddEditEntry = ({ navigation, route }) => {
{/* Stat */}
<View style={styles.nameView}>
{selectedStatType ? (
<Text style={{ ...GS.text, flex: 1 }}>
<Text style={{ ...GS.text, flex: 1, marginVertical: 6 }}>
{selectedStatType.name}
{selectedStatType.unit ? ` (${selectedStatType.unit})` : ''}
</Text>
) : (
<Text style={{ ...GS.text, flex: 1 }}>Stat</Text>
)}
{filteredStatTypes.length === 0 ? (
<Button onPress={onAddStatType} title="Create Stat" color="green" />
) : (
<Button onPress={() => setStatModalOpen(true)} title="Change Stat" color="blue" />
<Text style={{ ...GS.text, flex: 1, marginVertical: 6 }}>Stat</Text>
)}
{/* Don't show any button when entering stat values */}
{(!selectedStatType || !statValues.find((el) => el !== '')) &&
(filteredStatTypes.length === 0 ? (
<Button onPress={onAddStatType} title="Create Stat" color="green" />
) : (
<Button onPress={() => setStatModalOpen(true)} title="Change Stat" color="blue" />
))}
</View>
{statValues.map((value: string | number, index: number) => (
{statValues.map((value: string, index: number) => (
<TextInput
key={String(index)}
style={GS.input}
placeholder="Value"
placeholder={selectedStatType ? 'Value' : 'Please select stat type first'}
placeholderTextColor="grey"
multiline
editable={selectedStatType !== null}
keyboardType={selectedStatType?.variant === StatTypeVariant.NUMBER ? 'numeric' : 'default'}
value={String(value)}
value={value}
onChangeText={(val: string) => updateStatValues(index, val)}
/>
))}
Expand Down
25 changes: 17 additions & 8 deletions app/shared/GlobalFunctions.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { IDate } from './DataStructure';

// This outputs a text version of a given date. pretty = false means don't use the pretty
// formatting and include the time too. Used for backup file names.
export const formatDate = (date: Date, pretty = true): string => {
// Separator being unset means output the pretty date. In that case includeTime is ignored.
export const formatDate = (date: Date, separator = '', includeTime = false): string => {
let output = '';
const months = [
'January',
Expand All @@ -19,21 +18,31 @@ export const formatDate = (date: Date, pretty = true): string => {
'December',
];

if (pretty) {
if (!separator) {
output += date.getDate() + ' ';
output += months[date.getMonth()] + ' ';
output += date.getFullYear();
} else {
output += date.getFullYear() + '_';
output += date.getFullYear() + separator;
// The Date class saves months with 0 indexing
output += (date.getMonth() >= 9 ? '' : '0') + (date.getMonth() + 1) + '_';
output += (date.getDate() >= 10 ? '' : '0') + date.getDate() + '_';
output += date.toTimeString().slice(0, 8).replace(/:/g, '_');
output += (date.getMonth() >= 9 ? '' : '0') + (date.getMonth() + 1) + separator;
output += (date.getDate() >= 10 ? '' : '0') + date.getDate() + separator;
if (includeTime) output += date.toTimeString().slice(0, 8).replace(/:/g, separator);
}

return output;
};

export const formatIDate = (date: IDate, separator = '.'): string => {
let output = '';

output += (date.day >= 10 ? '' : '0') + date.day + separator;
output += (date.month >= 10 ? '' : '0') + date.month + separator;
output += date.year;

return output;
};

// Returns true if date1 is more recent or the same as date2
export const isNewerOrSameDate = (date1: IDate, date2: IDate): boolean => {
if (date1.year > date2.year) return true;
Expand Down
8 changes: 7 additions & 1 deletion app/shared/GlobalStyles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,16 +79,22 @@ export default StyleSheet.create({
padding: 16,
borderRadius: 10,
backgroundColor: 'pink',
shadowColor: '#000000',
shadowOpacity: 0.5,
elevation: 5,
},
smallCard: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
backgroundColor: 'pink',
marginBottom: 10,
paddingHorizontal: 14,
paddingVertical: 10,
borderRadius: 10,
backgroundColor: 'pink',
shadowColor: '#000000',
shadowOpacity: 0.5,
elevation: 5,
},
button: {
flex: 1,
Expand Down
Loading

0 comments on commit 1d4b59d

Please sign in to comment.