Skip to content

Commit

Permalink
moved meal summaries logic to dedicated context
Browse files Browse the repository at this point in the history
  • Loading branch information
ebbmango committed Jan 8, 2025
1 parent 0bf0557 commit 08c1606
Show file tree
Hide file tree
Showing 8 changed files with 218 additions and 126 deletions.
13 changes: 10 additions & 3 deletions App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,23 +9,30 @@ import { ColorsProvider } from 'context/ColorContext';
import { Suspense } from 'react';
import { Text } from 'react-native-ui-lib';
import { SQLiteProvider } from 'expo-sqlite';
import { MealSummariesProvider } from 'context/SummariesContext';

const queryClient = new QueryClient();

export default function App() {
return (
<GestureHandlerRootView>
{/* Enables access to the React Query's query client */}
<QueryClientProvider client={queryClient}>
{/* Enables access to the SQLite database */}
<SQLiteProvider
databaseName="database.db"
options={{ enableChangeListener: true }}
assetSource={{
assetId: require('assets/database/appDatabase.db'),
}}>
{/* Makes currently selected date available via context */}
<DateProvider>
<ColorsProvider>
<RootStack />
</ColorsProvider>
{/* Makes meals' summaries available via context */}
<MealSummariesProvider>
<ColorsProvider>
<RootStack />
</ColorsProvider>
</MealSummariesProvider>
</DateProvider>
</SQLiteProvider>
</QueryClientProvider>
Expand Down
12 changes: 6 additions & 6 deletions assets/database/dbSchema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -60,12 +60,12 @@ VALUES ('g', 1),
('cup', 6),
('oz', 7);
-- Inserts initial ("out-of-the-box") meals.
INSERT INTO meals (name, isDeleted)
VALUES ('breakfast', 0),
('morning snack', 0),
('lunch', 0),
('afternoon snack', 0),
('dinner', 0);
INSERT INTO meals (id, name, isDeleted)
VALUES (1, 'Breakfast', 0),
(2, 'Morning', 0),
(3, 'Lunch', 0),
(4, 'Afternoon', 0),
(5, 'Dinner', 0);
-- Inserts initial foods (for ease of development only - DELETE FOR PRODUCTION)
INSERT INTO foods (id, name, isDeleted)
VALUES (1, 'Milk', 0),
Expand Down
5 changes: 2 additions & 3 deletions components/MacroOverview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,10 @@ import { Colors, Text } from 'react-native-ui-lib';
type MacroOverviewProps = {
color: string;
iconName: 'meat-solid' | 'wheat-solid' | 'bacon-solid' | 'ball-pile-solid';
amount: number;
unit: string;
amount: string;
};

export default function MacroOverview({ color, iconName, amount, unit }: MacroOverviewProps) {
export default function MacroOverview({ color, iconName, amount }: MacroOverviewProps) {
return (
<Pressable
style={{
Expand Down
18 changes: 17 additions & 1 deletion components/Screens/Home/MealDrawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ import { getAllUnits } from 'database/queries/unitsQueries';

type MealDrawerProps = {
meal: Meal;
summaries: {
day: MacroSummary;
meal: MacroSummary;
};
};

type DrawerHeaderProps = {
Expand All @@ -28,6 +32,10 @@ type DrawerHeaderProps = {
type DrawerBodyProps = {
meal: Meal;
entries: EntrySummary[];
summaries: {
day: MacroSummary;
meal: MacroSummary;
};
};

type EntrySummary = {
Expand All @@ -39,7 +47,15 @@ type EntrySummary = {
kcals: number;
};

export default function MealDrawer({ meal }: MealDrawerProps) {
export type MacroSummary = {
mealId?: number;
kcals: number;
protein: number;
fat: number;
carbs: number;
};

export default function MealDrawer({ meal, summaries }: MealDrawerProps) {
const [expanded, setExpanded] = useState(false);

const date = useDate();
Expand Down
150 changes: 150 additions & 0 deletions context/SummariesContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import React, { createContext, useContext, ReactNode, useMemo } from 'react';
import { useSQLiteContext } from 'expo-sqlite';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { addDatabaseChangeListener } from 'expo-sqlite';

import { getEntriesByDate } from 'database/queries/entriesQueries';
import { getNutritablesByIds } from 'database/queries/nutritablesQueries';
import { Nutritable } from 'database/types';
import { useDate } from 'context/DateContext';

export type MacroSummary = {
mealId: 0 | 1 | 2 | 3 | 4 | 5;
kcals: number;
protein: number;
fat: number;
carbs: number;
};

type MealSummaries = Record<
| 'day' /* mealId: 0*/
| 'breakfast' /* mealId: 1*/
| 'morning' /* mealId: 2*/
| 'lunch' /* mealId: 3*/
| 'afternoon' /* mealId: 4*/
| 'dinner' /* mealId: 5*/,
MacroSummary
>;

type MealSummariesContextType = MealSummaries;
const MealSummariesContext = createContext<MealSummariesContextType | undefined>(undefined);

export function useMealSummaries() {
const context = useContext(MealSummariesContext);
if (!context) {
throw new Error('useMealSummaries must be used within a MealSummariesProvider');
}
return context;
}

type MealSummariesProviderProps = {
children: ReactNode;
};

export function MealSummariesProvider({ children }: MealSummariesProviderProps) {
const database = useSQLiteContext();
const queryClient = useQueryClient();
const date = useDate().get();

// Fetch relevant (date) entries
const {
data: entries = [],
refetch: refetchEntries,
isFetched: entriesFetched,
} = useQuery({
queryKey: ['entries'],
queryFn: () => getEntriesByDate(database, { date: date }),
initialData: [],
});

// Fetch relevant (entries) nutritables
const { data: nutritables = [], refetch: refetchNutritables } = useQuery({
queryKey: ['nutritables'],
queryFn: () => {
// Makes a set out of the used ids, making sure their values are unique.
const tableIds = new Set(entries.map((entry) => entry.nutritableId));
// Fetches each unique nutritable.
return getNutritablesByIds(database, { ids: Array.from(tableIds) });
},
initialData: [],
enabled: entriesFetched && entries.length > 0,
});

// Listen for database changes
React.useEffect(() => {
const listener = addDatabaseChangeListener((change) => {
if (change.tableName === 'entries') queryClient.invalidateQueries({ queryKey: ['entries'] });
if (change.tableName === 'nutritables')
queryClient.invalidateQueries({ queryKey: ['nutritables'] });
});
return () => {
listener.remove();
};
}, [refetchEntries, refetchNutritables]);

// Refetch when date changes
React.useEffect(() => {
refetchEntries();
}, [date, refetchEntries]);

// Refetch nutritables when entries change
React.useEffect(() => {
refetchNutritables();
}, [entries, refetchNutritables]);

// Calculate Summaries
const summaries: MealSummaries = useMemo(() => {
const empty = { kcals: 0, fat: 0, protein: 0, carbs: 0 };

const mealSummaries: MealSummaries = {
day: { mealId: 0, ...empty },
breakfast: { mealId: 1, ...empty },
morning: { mealId: 2, ...empty },
lunch: { mealId: 3, ...empty },
afternoon: { mealId: 4, ...empty },
dinner: { mealId: 5, ...empty },
};

if (entries.length === 0 || nutritables.length === 0) {
return mealSummaries;
}

const nutritableMap = new Map<number, Nutritable>();
nutritables.forEach((n) => nutritableMap.set(n.id, n));

entries.forEach((entry) => {
const nutritable = nutritableMap.get(entry.nutritableId);
if (!nutritable) return;

const ratio = entry.amount / nutritable.baseMeasure;

const mealSummary = Object.values(mealSummaries).find(
(summary) => summary.mealId === entry.mealId
);

if (mealSummary) {
mealSummary.kcals += nutritable.kcals * ratio;
mealSummary.fat += nutritable.fats * ratio;
mealSummary.protein += nutritable.protein * ratio;
mealSummary.carbs += nutritable.carbs * ratio;
}
});

// Calculate day summary
mealSummaries.day = Object.values(mealSummaries).reduce((total, meal) => ({
mealId: 0,
kcals: total.kcals + meal.kcals,
fat: total.fat + meal.fat,
protein: total.protein + meal.protein,
carbs: total.carbs + meal.carbs,
}));

return mealSummaries;
}, [entries, nutritables]);

return (
<MealSummariesContext.Provider value={{ ...summaries }}>
{children}
</MealSummariesContext.Provider>
);
}
4 changes: 3 additions & 1 deletion screens/Add.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,9 @@ export default function Add({ route }: Props) {
style={{ borderRadius: 12 }}
disabled={validation.status === ValidationStatus.Error}
label={
validation.status === ValidationStatus.Warning ? 'Proceed anyway' : 'Create food'
validation.status === ValidationStatus.Warning
? 'Proceed anyway'
: 'Add'
}
onPress={() => {
// Validates the current data
Expand Down
Loading

0 comments on commit 08c1606

Please sign in to comment.