Skip to content

Commit 6aa6ddb

Browse files
Refractor User/recipes page extract som components improve navigation
1 parent e2371d1 commit 6aa6ddb

20 files changed

Lines changed: 1710 additions & 1777 deletions
Lines changed: 305 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,305 @@
1+
import React, { useState, useEffect } from 'react';
2+
import { useNavigate } from '@tanstack/react-router';
3+
import { Plus } from 'lucide-react';
4+
import useAuthStore from '@/store/authStore';
5+
import RecipeService from '@/services/recipe.service';
6+
import ingredientService from '@/services/ingredient.service';
7+
import glassService from '@/services/glass.service';
8+
import { Button } from '@/components/ui/button';
9+
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
10+
import { toast } from 'sonner';
11+
import { Loader2 } from 'lucide-react';
12+
import { RecipeFormHeader } from './components/RecipeFormHeader';
13+
import { RecipeBasicInfo } from './components/RecipeBasicInfo';
14+
import { RecipeImageUpload } from './components/RecipeImageUpload';
15+
import { ProductionStepEditor } from './components/ProductionStepEditor';
16+
17+
// Types
18+
interface Ingredient {
19+
id: number;
20+
name: string;
21+
}
22+
23+
interface Glass {
24+
id: number;
25+
name: string;
26+
size: number;
27+
}
28+
29+
interface StepIngredient {
30+
ingredient: Ingredient | null;
31+
amount: number;
32+
scale: string;
33+
boostable?: boolean;
34+
}
35+
36+
interface ProductionStep {
37+
type: string;
38+
stepIngredients?: StepIngredient[];
39+
message?: string;
40+
}
41+
42+
export const NewRecipePage: React.FC = () => {
43+
const navigate = useNavigate();
44+
const token = useAuthStore((state) => state.token);
45+
46+
const [loading, setLoading] = useState(true);
47+
const [saving, setSaving] = useState(false);
48+
const [ingredients, setIngredients] = useState<Ingredient[]>([]);
49+
const [glasses, setGlasses] = useState<Glass[]>([]);
50+
51+
const [formData, setFormData] = useState({
52+
name: '',
53+
description: '',
54+
defaultGlass: null as Glass | null,
55+
defaultAmountToFill: 250,
56+
productionSteps: [] as ProductionStep[],
57+
image: null as File | null,
58+
imagePreview: null as string | null,
59+
});
60+
61+
useEffect(() => {
62+
if (token) {
63+
loadData();
64+
}
65+
}, [token]);
66+
67+
const loadData = async () => {
68+
try {
69+
setLoading(true);
70+
const [ingredientsData, glassesData] = await Promise.all([
71+
ingredientService.getIngredients(token),
72+
glassService.getGlasses(token),
73+
]);
74+
75+
setIngredients(ingredientsData);
76+
setGlasses(glassesData);
77+
} catch (error) {
78+
console.error('Failed to load data:', error);
79+
toast.error('Failed to load ingredients and glasses');
80+
} finally {
81+
setLoading(false);
82+
}
83+
};
84+
85+
const handleSubmit = async () => {
86+
if (!formData.name.trim()) {
87+
toast.error('Recipe name is required');
88+
return;
89+
}
90+
91+
try {
92+
setSaving(true);
93+
94+
const recipeData = new FormData();
95+
recipeData.append('name', formData.name);
96+
recipeData.append('description', formData.description);
97+
if (formData.defaultGlass) {
98+
recipeData.append('defaultGlass', formData.defaultGlass.id.toString());
99+
}
100+
recipeData.append(
101+
'defaultAmountToFill',
102+
formData.defaultAmountToFill.toString(),
103+
);
104+
105+
const productionStepsPayload = formData.productionSteps.map((step) => ({
106+
type: step.type,
107+
stepIngredients:
108+
step.stepIngredients?.map((si) => ({
109+
ingredient: si.ingredient ? { id: si.ingredient.id } : null,
110+
amount: si.amount,
111+
scale: si.scale,
112+
boostable: si.boostable,
113+
})) || [],
114+
}));
115+
116+
recipeData.append(
117+
'productionSteps',
118+
JSON.stringify(productionStepsPayload),
119+
);
120+
121+
if (formData.image) {
122+
recipeData.append('image', formData.image);
123+
}
124+
125+
await RecipeService.createRecipe(recipeData, token);
126+
toast.success('Recipe created successfully');
127+
navigate({ to: '/recipes' });
128+
} catch (error) {
129+
console.error('Failed to create recipe:', error);
130+
toast.error('Failed to create recipe');
131+
} finally {
132+
setSaving(false);
133+
}
134+
};
135+
136+
const addProductionStep = () => {
137+
setFormData({
138+
...formData,
139+
productionSteps: [
140+
...formData.productionSteps,
141+
{
142+
type: 'addIngredients',
143+
stepIngredients: [],
144+
},
145+
],
146+
});
147+
};
148+
149+
const removeProductionStep = (index: number) => {
150+
const newSteps = formData.productionSteps.filter((_, i) => i !== index);
151+
setFormData({ ...formData, productionSteps: newSteps });
152+
};
153+
154+
const addIngredientToStep = (stepIndex: number) => {
155+
const newSteps = [...formData.productionSteps];
156+
const step = newSteps[stepIndex];
157+
if (step.type === 'addIngredients') {
158+
step.stepIngredients = [
159+
...(step.stepIngredients || []),
160+
{
161+
ingredient: ingredients[0],
162+
amount: 30,
163+
scale: 'ml',
164+
boostable: false,
165+
},
166+
];
167+
setFormData({ ...formData, productionSteps: newSteps });
168+
}
169+
};
170+
171+
const updateStepIngredient = (
172+
stepIndex: number,
173+
ingredientIndex: number,
174+
field: keyof StepIngredient,
175+
value: any,
176+
) => {
177+
const newSteps = [...formData.productionSteps];
178+
const step = newSteps[stepIndex];
179+
if (step.type === 'addIngredients' && step.stepIngredients) {
180+
step.stepIngredients[ingredientIndex] = {
181+
...step.stepIngredients[ingredientIndex],
182+
[field]: value,
183+
};
184+
setFormData({ ...formData, productionSteps: newSteps });
185+
}
186+
};
187+
188+
const removeIngredientFromStep = (
189+
stepIndex: number,
190+
ingredientIndex: number,
191+
) => {
192+
const newSteps = [...formData.productionSteps];
193+
const step = newSteps[stepIndex];
194+
if (step.type === 'addIngredients' && step.stepIngredients) {
195+
step.stepIngredients = step.stepIngredients.filter(
196+
(_, i) => i !== ingredientIndex,
197+
);
198+
setFormData({ ...formData, productionSteps: newSteps });
199+
}
200+
};
201+
202+
if (loading) {
203+
return (
204+
<div className="flex items-center justify-center h-full">
205+
<Loader2 className="h-8 w-8 animate-spin" />
206+
</div>
207+
);
208+
}
209+
210+
return (
211+
<div className="min-h-screen bg-background">
212+
<RecipeFormHeader
213+
title="Create New Recipe"
214+
onSave={handleSubmit}
215+
saving={saving}
216+
saveText="Create Recipe"
217+
/>
218+
219+
<div className="container mx-auto px-4 py-6 max-w-5xl">
220+
<form onSubmit={handleSubmit} className="space-y-6">
221+
<RecipeBasicInfo
222+
name={formData.name}
223+
description={formData.description}
224+
defaultGlass={formData.defaultGlass}
225+
defaultAmountToFill={formData.defaultAmountToFill}
226+
glasses={glasses}
227+
onUpdate={(field, value) => setFormData({ ...formData, [field]: value })}
228+
/>
229+
230+
<Card>
231+
<CardHeader>
232+
<CardTitle>Recipe Image</CardTitle>
233+
</CardHeader>
234+
<CardContent>
235+
<RecipeImageUpload
236+
imagePreview={formData.imagePreview}
237+
onImageChange={(file) => setFormData({ ...formData, image: file, imagePreview: URL.createObjectURL(file) })}
238+
onRemoveImage={() => setFormData({ ...formData, image: null, imagePreview: null })}
239+
/>
240+
</CardContent>
241+
</Card>
242+
243+
<Card>
244+
<CardHeader>
245+
<div className="flex items-center justify-between">
246+
<CardTitle>Production Steps</CardTitle>
247+
<Button type="button" onClick={addProductionStep} size="sm">
248+
<Plus className="mr-2 h-4 w-4" />
249+
Add Step
250+
</Button>
251+
</div>
252+
</CardHeader>
253+
<CardContent className="space-y-4">
254+
{formData.productionSteps.length === 0 ? (
255+
<p className="text-muted-foreground text-center py-4">
256+
No production steps added yet. Click "Add Step" to begin.
257+
</p>
258+
) : (
259+
formData.productionSteps.map((step, stepIndex) => (
260+
<ProductionStepEditor
261+
key={stepIndex}
262+
step={step}
263+
stepIndex={stepIndex}
264+
ingredients={ingredients}
265+
onUpdateStep={(updatedStep) => {
266+
const newSteps = [...formData.productionSteps];
267+
newSteps[stepIndex] = updatedStep;
268+
setFormData({ ...formData, productionSteps: newSteps });
269+
}}
270+
onRemoveStep={() => removeProductionStep(stepIndex)}
271+
onAddIngredient={() => addIngredientToStep(stepIndex)}
272+
onRemoveIngredient={(ingredientIndex) => removeIngredientFromStep(stepIndex, ingredientIndex)}
273+
onUpdateIngredient={(ingredientIndex, field, value) => updateStepIngredient(stepIndex, ingredientIndex, field, value)}
274+
/>
275+
))
276+
)}
277+
</CardContent>
278+
</Card>
279+
280+
<div className="flex gap-4">
281+
<Button type="submit" disabled={saving} className="flex-1">
282+
{saving ? (
283+
<>
284+
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
285+
Creating...
286+
</>
287+
) : (
288+
<>
289+
Save
290+
</>
291+
)}
292+
</Button>
293+
<Button
294+
type="button"
295+
variant="outline"
296+
onClick={() => navigate({ to: '/recipes' })}
297+
>
298+
Cancel
299+
</Button>
300+
</div>
301+
</form>
302+
</div>
303+
</div>
304+
);
305+
};

0 commit comments

Comments
 (0)