Add made by

This commit is contained in:
dvirlabs 2025-12-05 15:47:47 +02:00
parent 56d00c2ed8
commit e0b0ec94fe
11 changed files with 121 additions and 10 deletions

View File

@ -55,7 +55,7 @@ def list_recipes_db() -> List[Dict[str, Any]]:
cur.execute( cur.execute(
""" """
SELECT id, name, meal_type, time_minutes, SELECT id, name, meal_type, time_minutes,
tags, ingredients, steps, image tags, ingredients, steps, image, made_by
FROM recipes FROM recipes
ORDER BY id ORDER BY id
""" """
@ -68,7 +68,7 @@ def list_recipes_db() -> List[Dict[str, Any]]:
def update_recipe_db(recipe_id: int, recipe_data: Dict[str, Any]) -> Optional[Dict[str, Any]]: def update_recipe_db(recipe_id: int, recipe_data: Dict[str, Any]) -> Optional[Dict[str, Any]]:
""" """
עדכון מתכון קיים לפי id. עדכון מתכון קיים לפי id.
recipe_data: name, meal_type, time_minutes, tags, ingredients, steps, image recipe_data: name, meal_type, time_minutes, tags, ingredients, steps, image, made_by
""" """
conn = get_conn() conn = get_conn()
try: try:
@ -82,9 +82,10 @@ def update_recipe_db(recipe_id: int, recipe_data: Dict[str, Any]) -> Optional[Di
tags = %s, tags = %s,
ingredients = %s, ingredients = %s,
steps = %s, steps = %s,
image = %s image = %s,
made_by = %s
WHERE id = %s WHERE id = %s
RETURNING id, name, meal_type, time_minutes, tags, ingredients, steps, image RETURNING id, name, meal_type, time_minutes, tags, ingredients, steps, image, made_by
""", """,
( (
recipe_data["name"], recipe_data["name"],
@ -94,6 +95,7 @@ def update_recipe_db(recipe_id: int, recipe_data: Dict[str, Any]) -> Optional[Di
json.dumps(recipe_data.get("ingredients", [])), json.dumps(recipe_data.get("ingredients", [])),
json.dumps(recipe_data.get("steps", [])), json.dumps(recipe_data.get("steps", [])),
recipe_data.get("image"), recipe_data.get("image"),
recipe_data.get("made_by"),
recipe_id, recipe_id,
), ),
) )
@ -131,9 +133,9 @@ def create_recipe_db(recipe_data: Dict[str, Any]) -> Dict[str, Any]:
with conn.cursor() as cur: with conn.cursor() as cur:
cur.execute( cur.execute(
""" """
INSERT INTO recipes (name, meal_type, time_minutes, tags, ingredients, steps, image) INSERT INTO recipes (name, meal_type, time_minutes, tags, ingredients, steps, image, made_by)
VALUES (%s, %s, %s, %s, %s, %s, %s) VALUES (%s, %s, %s, %s, %s, %s, %s, %s)
RETURNING id, name, meal_type, time_minutes, tags, ingredients, steps, image RETURNING id, name, meal_type, time_minutes, tags, ingredients, steps, image, made_by
""", """,
( (
recipe_data["name"], recipe_data["name"],
@ -143,6 +145,7 @@ def create_recipe_db(recipe_data: Dict[str, Any]) -> Dict[str, Any]:
json.dumps(recipe_data.get("ingredients", [])), json.dumps(recipe_data.get("ingredients", [])),
json.dumps(recipe_data.get("steps", [])), json.dumps(recipe_data.get("steps", [])),
recipe_data.get("image"), recipe_data.get("image"),
recipe_data.get("made_by"),
), ),
) )
row = cur.fetchone() row = cur.fetchone()
@ -160,7 +163,7 @@ def get_recipes_by_filters_db(
try: try:
query = """ query = """
SELECT id, name, meal_type, time_minutes, SELECT id, name, meal_type, time_minutes,
tags, ingredients, steps tags, ingredients, steps, image, made_by
FROM recipes FROM recipes
WHERE 1=1 WHERE 1=1
""" """

View File

@ -21,6 +21,7 @@ class RecipeBase(BaseModel):
name: str name: str
meal_type: str # breakfast / lunch / dinner / snack meal_type: str # breakfast / lunch / dinner / snack
time_minutes: int time_minutes: int
made_by: Optional[str] = None # Person who created this recipe version
tags: List[str] = [] tags: List[str] = []
ingredients: List[str] = [] ingredients: List[str] = []
steps: List[str] = [] steps: List[str] = []
@ -63,6 +64,7 @@ def list_recipes():
name=r["name"], name=r["name"],
meal_type=r["meal_type"], meal_type=r["meal_type"],
time_minutes=r["time_minutes"], time_minutes=r["time_minutes"],
made_by=r.get("made_by"),
tags=r["tags"] or [], tags=r["tags"] or [],
ingredients=r["ingredients"] or [], ingredients=r["ingredients"] or [],
steps=r["steps"] or [], steps=r["steps"] or [],
@ -85,6 +87,7 @@ def create_recipe(recipe_in: RecipeCreate):
name=row["name"], name=row["name"],
meal_type=row["meal_type"], meal_type=row["meal_type"],
time_minutes=row["time_minutes"], time_minutes=row["time_minutes"],
made_by=row.get("made_by"),
tags=row["tags"] or [], tags=row["tags"] or [],
ingredients=row["ingredients"] or [], ingredients=row["ingredients"] or [],
steps=row["steps"] or [], steps=row["steps"] or [],
@ -105,6 +108,7 @@ def update_recipe(recipe_id: int, recipe_in: RecipeUpdate):
name=row["name"], name=row["name"],
meal_type=row["meal_type"], meal_type=row["meal_type"],
time_minutes=row["time_minutes"], time_minutes=row["time_minutes"],
made_by=row.get("made_by"),
tags=row["tags"] or [], tags=row["tags"] or [],
ingredients=row["ingredients"] or [], ingredients=row["ingredients"] or [],
steps=row["steps"] or [], steps=row["steps"] or [],
@ -137,6 +141,7 @@ def random_recipe(
name=r["name"], name=r["name"],
meal_type=r["meal_type"], meal_type=r["meal_type"],
time_minutes=r["time_minutes"], time_minutes=r["time_minutes"],
made_by=r.get("made_by"),
tags=r["tags"] or [], tags=r["tags"] or [],
ingredients=r["ingredients"] or [], ingredients=r["ingredients"] or [],
steps=r["steps"] or [], steps=r["steps"] or [],

View File

@ -4,7 +4,7 @@ CREATE TABLE IF NOT EXISTS recipes (
name TEXT NOT NULL, name TEXT NOT NULL,
meal_type TEXT NOT NULL, -- breakfast / lunch / dinner / snack meal_type TEXT NOT NULL, -- breakfast / lunch / dinner / snack
time_minutes INTEGER NOT NULL, time_minutes INTEGER NOT NULL,
made_by TEXT, -- Person who created this recipe version
tags JSONB NOT NULL DEFAULT '[]', -- ["מהיר", "בריא"] tags JSONB NOT NULL DEFAULT '[]', -- ["מהיר", "בריא"]
ingredients JSONB NOT NULL DEFAULT '[]', -- ["ביצה", "עגבניה", "מלח"] ingredients JSONB NOT NULL DEFAULT '[]', -- ["ביצה", "עגבניה", "מלח"]
steps JSONB NOT NULL DEFAULT '[]', -- ["לחתוך", "לבשל", ...] steps JSONB NOT NULL DEFAULT '[]', -- ["לחתוך", "לבשל", ...]
@ -18,8 +18,12 @@ CREATE INDEX IF NOT EXISTS idx_recipes_meal_type
CREATE INDEX IF NOT EXISTS idx_recipes_time_minutes CREATE INDEX IF NOT EXISTS idx_recipes_time_minutes
ON recipes (time_minutes); ON recipes (time_minutes);
CREATE INDEX IF NOT EXISTS idx_recipes_made_by
ON recipes (made_by);
CREATE INDEX IF NOT EXISTS idx_recipes_tags_jsonb CREATE INDEX IF NOT EXISTS idx_recipes_tags_jsonb
ON recipes USING GIN (tags); ON recipes USING GIN (tags);
CREATE INDEX IF NOT EXISTS idx_recipes_ingredients_jsonb CREATE INDEX IF NOT EXISTS idx_recipes_ingredients_jsonb
ON recipes USING GIN (ingredients); ON recipes USING GIN (ingredients);

View File

@ -314,6 +314,13 @@ select {
color: var(--text-muted); color: var(--text-muted);
} }
.recipe-made-by {
margin: 0.3rem 0 0;
font-size: 0.8rem;
color: var(--accent);
font-weight: 500;
}
.pill-row { .pill-row {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;

View File

@ -19,6 +19,7 @@ function App() {
const [filterMealType, setFilterMealType] = useState(""); const [filterMealType, setFilterMealType] = useState("");
const [filterMaxTime, setFilterMaxTime] = useState(""); const [filterMaxTime, setFilterMaxTime] = useState("");
const [filterTags, setFilterTags] = useState([]); const [filterTags, setFilterTags] = useState([]);
const [filterMadeBy, setFilterMadeBy] = useState("");
// Random recipe filters // Random recipe filters
const [mealTypeFilter, setMealTypeFilter] = useState(""); const [mealTypeFilter, setMealTypeFilter] = useState("");
@ -95,6 +96,11 @@ function App() {
} }
} }
// Filter by made_by
if (filterMadeBy && (!recipe.made_by || recipe.made_by !== filterMadeBy)) {
return false;
}
return true; return true;
}); });
}; };
@ -222,6 +228,8 @@ function App() {
onMaxTimeChange={setFilterMaxTime} onMaxTimeChange={setFilterMaxTime}
filterTags={filterTags} filterTags={filterTags}
onTagsChange={setFilterTags} onTagsChange={setFilterTags}
filterMadeBy={filterMadeBy}
onMadeByChange={setFilterMadeBy}
/> />
</section> </section>

View File

@ -26,6 +26,9 @@ function RecipeDetails({ recipe, onEditClick, onDeleteClick, onShowDeleteModal }
<p className="recipe-subtitle"> <p className="recipe-subtitle">
{translateMealType(recipe.meal_type)} · {recipe.time_minutes} דקות הכנה {translateMealType(recipe.meal_type)} · {recipe.time_minutes} דקות הכנה
</p> </p>
{recipe.made_by && (
<h4 className="recipe-made-by">המתכון של: {recipe.made_by}</h4>
)}
</div> </div>
<div className="pill-row"> <div className="pill-row">
<span className="pill"> {recipe.time_minutes} דק׳</span> <span className="pill"> {recipe.time_minutes} דק׳</span>

View File

@ -4,6 +4,7 @@ function RecipeFormDrawer({ open, onClose, onSubmit, editingRecipe = null }) {
const [name, setName] = useState(""); const [name, setName] = useState("");
const [mealType, setMealType] = useState("lunch"); const [mealType, setMealType] = useState("lunch");
const [timeMinutes, setTimeMinutes] = useState(15); const [timeMinutes, setTimeMinutes] = useState(15);
const [madeBy, setMadeBy] = useState("");
const [tags, setTags] = useState(""); const [tags, setTags] = useState("");
const [image, setImage] = useState(""); const [image, setImage] = useState("");
@ -18,6 +19,7 @@ function RecipeFormDrawer({ open, onClose, onSubmit, editingRecipe = null }) {
setName(editingRecipe.name || ""); setName(editingRecipe.name || "");
setMealType(editingRecipe.meal_type || "lunch"); setMealType(editingRecipe.meal_type || "lunch");
setTimeMinutes(editingRecipe.time_minutes || 15); setTimeMinutes(editingRecipe.time_minutes || 15);
setMadeBy(editingRecipe.made_by || "");
setTags((editingRecipe.tags || []).join(", ")); setTags((editingRecipe.tags || []).join(", "));
setImage(editingRecipe.image || ""); setImage(editingRecipe.image || "");
setIngredients(editingRecipe.ingredients || [""]); setIngredients(editingRecipe.ingredients || [""]);
@ -26,6 +28,7 @@ function RecipeFormDrawer({ open, onClose, onSubmit, editingRecipe = null }) {
setName(""); setName("");
setMealType("lunch"); setMealType("lunch");
setTimeMinutes(15); setTimeMinutes(15);
setMadeBy("");
setTags(""); setTags("");
setImage(""); setImage("");
setIngredients([""]); setIngredients([""]);
@ -94,6 +97,10 @@ function RecipeFormDrawer({ open, onClose, onSubmit, editingRecipe = null }) {
steps: cleanSteps, steps: cleanSteps,
}; };
if (madeBy.trim()) {
payload.made_by = madeBy.trim();
}
if (image) { if (image) {
payload.image = image; payload.image = image;
} }
@ -145,6 +152,15 @@ function RecipeFormDrawer({ open, onClose, onSubmit, editingRecipe = null }) {
</div> </div>
</div> </div>
<div className="field">
<label>המתכון של:</label>
<input
value={madeBy}
onChange={(e) => setMadeBy(e.target.value)}
placeholder="שם האדם שיצר את הגרסה הזו..."
/>
</div>
<div className="field"> <div className="field">
<label>תמונה של המתכון</label> <label>תמונה של המתכון</label>
<div className="image-upload-wrapper"> <div className="image-upload-wrapper">

View File

@ -14,6 +14,8 @@ function RecipeSearchList({
onMaxTimeChange, onMaxTimeChange,
filterTags, filterTags,
onTagsChange, onTagsChange,
filterMadeBy,
onMadeByChange,
}) { }) {
const [expandFilters, setExpandFilters] = useState(false); const [expandFilters, setExpandFilters] = useState(false);
@ -25,6 +27,9 @@ function RecipeSearchList({
// Extract unique meal types from ALL recipes (not filtered) // Extract unique meal types from ALL recipes (not filtered)
const mealTypes = Array.from(new Set(allRecipes.map((r) => r.meal_type))).sort(); const mealTypes = Array.from(new Set(allRecipes.map((r) => r.meal_type))).sort();
// Extract unique made_by from ALL recipes (not filtered)
const allMadeBy = Array.from(new Set(allRecipes.map((r) => r.made_by).filter(Boolean))).sort();
// Extract max time for slider from ALL recipes (not filtered) - add 10 to make it comfortable to select max time recipes // Extract max time for slider from ALL recipes (not filtered) - add 10 to make it comfortable to select max time recipes
const maxTimeInRecipes = Math.max(...allRecipes.map((r) => r.time_minutes), 0); const maxTimeInRecipes = Math.max(...allRecipes.map((r) => r.time_minutes), 0);
const sliderMax = maxTimeInRecipes > 0 ? maxTimeInRecipes + 10 : 70; const sliderMax = maxTimeInRecipes > 0 ? maxTimeInRecipes + 10 : 70;
@ -42,9 +47,10 @@ function RecipeSearchList({
onMealTypeChange(""); onMealTypeChange("");
onMaxTimeChange(""); onMaxTimeChange("");
onTagsChange([]); onTagsChange([]);
onMadeByChange("");
}; };
const hasActiveFilters = searchQuery || filterMealType || filterMaxTime || filterTags.length > 0; const hasActiveFilters = searchQuery || filterMealType || filterMaxTime || filterTags.length > 0 || filterMadeBy;
return ( return (
<section className="panel secondary recipe-search-list"> <section className="panel secondary recipe-search-list">
@ -152,6 +158,30 @@ function RecipeSearchList({
</div> </div>
</div> </div>
)} )}
{/* Made By Filter */}
{allMadeBy.length > 0 && (
<div className="filter-group">
<label className="filter-label">המתכונים של:</label>
<div className="filter-options">
<button
className={`filter-btn ${filterMadeBy === "" ? "active" : ""}`}
onClick={() => onMadeByChange("")}
>
הכל
</button>
{allMadeBy.map((person) => (
<button
key={person}
className={`filter-btn ${filterMadeBy === person ? "active" : ""}`}
onClick={() => onMadeByChange(person)}
>
{person}
</button>
))}
</div>
</div>
)}
</div> </div>
{/* Clear All Button */} {/* Clear All Button */}

View File

@ -0,0 +1,35 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .Release.Name }}-db-schema
namespace: {{ .Values.global.namespace }}
data:
schema.sql: |
-- Create recipes table
CREATE TABLE IF NOT EXISTS recipes (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
meal_type TEXT NOT NULL,
time_minutes INTEGER NOT NULL,
made_by TEXT,
tags JSONB NOT NULL DEFAULT '[]',
ingredients JSONB NOT NULL DEFAULT '[]',
steps JSONB NOT NULL DEFAULT '[]',
image TEXT
);
CREATE INDEX IF NOT EXISTS idx_recipes_meal_type
ON recipes (meal_type);
CREATE INDEX IF NOT EXISTS idx_recipes_time_minutes
ON recipes (time_minutes);
CREATE INDEX IF NOT EXISTS idx_recipes_made_by
ON recipes (made_by);
CREATE INDEX IF NOT EXISTS idx_recipes_tags_jsonb
ON recipes USING GIN (tags);
CREATE INDEX IF NOT EXISTS idx_recipes_ingredients_jsonb
ON recipes USING GIN (ingredients);