-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathApp.tsx
More file actions
229 lines (204 loc) · 8.61 KB
/
App.tsx
File metadata and controls
229 lines (204 loc) · 8.61 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
import React, { useState, useCallback, useEffect } from 'react';
import Layout from './components/Layout';
import FridgeScanner from './components/FridgeScanner';
import RecipeCard from './components/RecipeCard';
import StepByStepMode from './components/StepByStepMode';
import ShoppingList from './components/ShoppingList';
import PantryView from './components/PantryView';
import { Recipe, DietaryRestriction, AppState, AppTab } from './types';
import { analyzeFridgeImage, generateRecipes } from './services/geminiService';
const App: React.FC = () => {
const [state, setState] = useState<AppState>({
scannedIngredients: [],
pantryIngredients: [],
recipes: [],
favoriteRecipes: [],
shoppingList: [],
filter: 'None',
isScanning: false,
selectedRecipe: null
});
const [activeTab, setActiveTab] = useState<AppTab>('fridge');
// Initialization & Sync
useEffect(() => {
const pantry = localStorage.getItem('pantry_list');
const favorites = localStorage.getItem('favorite_recipes');
const shopping = localStorage.getItem('shopping_list');
setState(prev => ({
...prev,
pantryIngredients: pantry ? JSON.parse(pantry) : [],
favoriteRecipes: favorites ? JSON.parse(favorites) : [],
shoppingList: shopping ? JSON.parse(shopping) : []
}));
}, []);
useEffect(() => {
localStorage.setItem('pantry_list', JSON.stringify(state.pantryIngredients));
}, [state.pantryIngredients]);
useEffect(() => {
localStorage.setItem('favorite_recipes', JSON.stringify(state.favoriteRecipes));
}, [state.favoriteRecipes]);
useEffect(() => {
localStorage.setItem('shopping_list', JSON.stringify(state.shoppingList));
}, [state.shoppingList]);
const handleScan = async (base64Image: string) => {
setState(prev => ({ ...prev, isScanning: true }));
try {
const ingredients = await analyzeFridgeImage(base64Image);
const recipes = await generateRecipes(ingredients, state.pantryIngredients, state.filter);
setState(prev => ({
...prev,
scannedIngredients: ingredients,
recipes: recipes,
isScanning: false
}));
} catch (err) {
console.error("Scan failed", err);
setState(prev => ({ ...prev, isScanning: false }));
alert("Something went wrong analyzing the image. Please try again.");
}
};
const updateRecipesWithFilter = useCallback(async (newFilter: DietaryRestriction) => {
if (state.scannedIngredients.length === 0) return;
setState(prev => ({ ...prev, isScanning: true, filter: newFilter }));
try {
const recipes = await generateRecipes(state.scannedIngredients, state.pantryIngredients, newFilter);
setState(prev => ({
...prev,
recipes: recipes,
isScanning: false
}));
} catch (err) {
console.error("Filter update failed", err);
setState(prev => ({ ...prev, isScanning: false }));
}
}, [state.scannedIngredients, state.pantryIngredients]);
const handleFilterChange = (filter: DietaryRestriction) => {
setState(prev => ({ ...prev, filter }));
if (state.scannedIngredients.length > 0) {
updateRecipesWithFilter(filter);
}
};
const handleAddToShoppingList = (ingredient: string) => {
setState(prev => {
if (prev.shoppingList.includes(ingredient)) return prev;
return { ...prev, shoppingList: [...prev.shoppingList, ingredient] };
});
};
const toggleFavorite = (e: React.MouseEvent, recipe: Recipe) => {
e.stopPropagation();
setState(prev => {
const isFav = prev.favoriteRecipes.some(r => r.id === recipe.id);
if (isFav) {
return { ...prev, favoriteRecipes: prev.favoriteRecipes.filter(r => r.id !== recipe.id) };
} else {
return { ...prev, favoriteRecipes: [...prev.favoriteRecipes, recipe] };
}
});
};
return (
<Layout
activeFilter={state.filter}
onFilterChange={handleFilterChange}
onTabChange={setActiveTab}
activeTab={activeTab}
shoppingCount={state.shoppingList.length}
favoritesCount={state.favoriteRecipes.length}
>
{activeTab === 'fridge' && (
<div className="space-y-10">
<header>
<h2 className="text-3xl font-bold tracking-tight text-slate-900">What's in your fridge?</h2>
<p className="text-gray-500 mt-2">AI-powered suggestions using your fridge scan + pantry items.</p>
</header>
<FridgeScanner onScan={handleScan} isLoading={state.isScanning} />
{state.scannedIngredients.length > 0 && !state.isScanning && (
<section className="space-y-6 animate-in slide-in-from-bottom duration-500">
<div className="flex items-center justify-between">
<h3 className="text-xl font-bold flex items-center gap-2">
<i className="fa-solid fa-sparkles text-amber-500"></i>
Tailored Suggestions
</h3>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{state.recipes.map(recipe => (
<RecipeCard
key={recipe.id}
recipe={recipe}
isFavorite={state.favoriteRecipes.some(r => r.id === recipe.id)}
onSelect={(r) => setState(prev => ({ ...prev, selectedRecipe: r }))}
onToggleFavorite={toggleFavorite}
/>
))}
</div>
</section>
)}
{state.scannedIngredients.length > 0 && (
<div className="bg-emerald-50 rounded-2xl p-6 border border-emerald-100">
<h4 className="font-bold text-emerald-800 mb-3 text-sm uppercase tracking-wide">Detected in Photo</h4>
<div className="flex flex-wrap gap-2">
{state.scannedIngredients.map((item, idx) => (
<span key={idx} className="bg-white px-3 py-1 rounded-full text-xs font-medium text-emerald-700 shadow-sm border border-emerald-100">
{item}
</span>
))}
</div>
</div>
)}
</div>
)}
{activeTab === 'favorites' && (
<div className="space-y-10 animate-in fade-in duration-300">
<header>
<h2 className="text-3xl font-bold tracking-tight text-slate-900">Favorite Recipes</h2>
<p className="text-gray-500 mt-2">Quick access to the meals you love.</p>
</header>
{state.favoriteRecipes.length === 0 ? (
<div className="text-center py-32 bg-white rounded-3xl border border-gray-100 shadow-sm">
<i className="fa-solid fa-heart text-gray-100 text-6xl mb-6 block"></i>
<p className="text-gray-400 font-medium">No favorites yet.</p>
<button onClick={() => setActiveTab('fridge')} className="mt-4 text-emerald-600 font-bold hover:underline">Start scanning to find some!</button>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{state.favoriteRecipes.map(recipe => (
<RecipeCard
key={recipe.id}
recipe={recipe}
isFavorite={true}
onSelect={(r) => setState(prev => ({ ...prev, selectedRecipe: r }))}
onToggleFavorite={toggleFavorite}
/>
))}
</div>
)}
</div>
)}
{activeTab === 'pantry' && (
<PantryView
ingredients={state.pantryIngredients}
onAdd={(item) => setState(prev => ({ ...prev, pantryIngredients: [...prev.pantryIngredients, item] }))}
onRemove={(item) => setState(prev => ({ ...prev, pantryIngredients: prev.pantryIngredients.filter(i => i !== item) }))}
onClear={() => setState(prev => ({ ...prev, pantryIngredients: [] }))}
/>
)}
{activeTab === 'shopping' && (
<div className="animate-in slide-in-from-right duration-300">
<ShoppingList
items={state.shoppingList}
onRemove={(item) => setState(prev => ({ ...prev, shoppingList: prev.shoppingList.filter(i => i !== item) }))}
onClear={() => setState(prev => ({ ...prev, shoppingList: [] }))}
/>
</div>
)}
{state.selectedRecipe && (
<StepByStepMode
recipe={state.selectedRecipe}
shoppingList={state.shoppingList}
onClose={() => setState(prev => ({ ...prev, selectedRecipe: null }))}
onAddToShoppingList={handleAddToShoppingList}
/>
)}
</Layout>
);
};
export default App;