Compare commits

..

No commits in common. "master" and "add-pic" have entirely different histories.

24 changed files with 72 additions and 302 deletions

View File

@ -1,24 +1,24 @@
steps: steps:
build-frontend: # build-frontend:
name: Build & Push Frontend # name: Build & Push Frontend
image: woodpeckerci/plugin-kaniko # image: woodpeckerci/plugin-kaniko
when: # when:
branch: [ master, develop ] # branch: [ master, develop ]
event: [ push, pull_request, tag ] # event: [ push, pull_request, tag ]
path: # path:
include: [ frontend/** ] # include: [ frontend/** ]
settings: # settings:
registry: harbor.dvirlabs.com # registry: harbor.dvirlabs.com
repo: my-apps/${CI_REPO_NAME}-frontend # repo: my-apps/${CI_REPO_NAME}-frontend
dockerfile: frontend/Dockerfile # dockerfile: frontend/Dockerfile
context: frontend # context: frontend
tags: # tags:
- latest # - latest
- ${CI_COMMIT_TAG:-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:7}} # - ${CI_COMMIT_TAG:-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:7}}
username: # username:
from_secret: DOCKER_USERNAME # from_secret: DOCKER_USERNAME
password: # password:
from_secret: DOCKER_PASSWORD # from_secret: DOCKER_PASSWORD
build-backend: build-backend:
name: Build & Push Backend name: Build & Push Backend
@ -41,32 +41,32 @@ steps:
password: password:
from_secret: DOCKER_PASSWORD from_secret: DOCKER_PASSWORD
update-values-frontend: # update-values-frontend:
name: Update frontend tag in values.yaml # name: Update frontend tag in values.yaml
image: alpine:3.19 # image: alpine:3.19
when: # when:
branch: [ master, develop ] # branch: [ master, develop ]
event: [ push ] # event: [ push ]
path: # path:
include: [ frontend/** ] # include: [ frontend/** ]
environment: # environment:
GIT_USERNAME: # GIT_USERNAME:
from_secret: GIT_USERNAME # from_secret: GIT_USERNAME
GIT_TOKEN: # GIT_TOKEN:
from_secret: GIT_TOKEN # from_secret: GIT_TOKEN
commands: # commands:
- apk add --no-cache git yq # - apk add --no-cache git yq
- git config --global user.name "woodpecker-bot" # - git config --global user.name "woodpecker-bot"
- git config --global user.email "ci@dvirlabs.com" # - git config --global user.email "ci@dvirlabs.com"
- git clone "https://$${GIT_USERNAME}:$${GIT_TOKEN}@git.dvirlabs.com/dvirlabs/my-apps.git" # - git clone "https://$${GIT_USERNAME}:$${GIT_TOKEN}@git.dvirlabs.com/dvirlabs/my-apps.git"
- cd my-apps # - cd my-apps
- | # - |
TAG="${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:7}" # TAG="${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:7}"
echo "💡 Setting frontend tag to: $TAG" # echo "💡 Setting frontend tag to: $TAG"
yq -i ".frontend.tag = \"$TAG\"" manifests/${CI_REPO_NAME}/values.yaml # yq -i ".frontend.tag = \"$TAG\"" manifests/${CI_REPO_NAME}/values.yaml
git add manifests/${CI_REPO_NAME}/values.yaml # git add manifests/${CI_REPO_NAME}/values.yaml
git commit -m "frontend: update tag to $TAG" || echo "No changes" # git commit -m "frontend: update tag to $TAG" || echo "No changes"
git push origin HEAD # git push origin HEAD
update-values-backend: update-values-backend:
name: Update backend tag in values.yaml name: Update backend tag in values.yaml

View File

@ -1,11 +0,0 @@
__pycache__
*.pyc
*.pyo
.git
.gitignore
.env
.env.local
.DS_Store
.pytest_cache
venv
env

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, made_by tags, ingredients, steps, image
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, made_by recipe_data: name, meal_type, time_minutes, tags, ingredients, steps, image
""" """
conn = get_conn() conn = get_conn()
try: try:
@ -82,10 +82,9 @@ 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, made_by RETURNING id, name, meal_type, time_minutes, tags, ingredients, steps, image
""", """,
( (
recipe_data["name"], recipe_data["name"],
@ -95,7 +94,6 @@ 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,
), ),
) )
@ -133,9 +131,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, made_by) INSERT INTO recipes (name, meal_type, time_minutes, tags, ingredients, steps, image)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s) VALUES (%s, %s, %s, %s, %s, %s, %s)
RETURNING id, name, meal_type, time_minutes, tags, ingredients, steps, image, made_by RETURNING id, name, meal_type, time_minutes, tags, ingredients, steps, image
""", """,
( (
recipe_data["name"], recipe_data["name"],
@ -145,7 +143,6 @@ 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()
@ -163,7 +160,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, image, made_by tags, ingredients, steps
FROM recipes FROM recipes
WHERE 1=1 WHERE 1=1
""" """

View File

@ -21,7 +21,6 @@ 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] = []
@ -46,17 +45,9 @@ app = FastAPI(
description="API פשוט לבחירת מתכונים לצהריים / ערב / כל ארוחה 😋", description="API פשוט לבחירת מתכונים לצהריים / ערב / כל ארוחה 😋",
) )
# Allow CORS from frontend domains
allowed_origins = [
"http://localhost:5173",
"http://localhost:3000",
"https://my-recipes.dvirlabs.com",
"http://my-recipes.dvirlabs.com",
]
app.add_middleware( app.add_middleware(
CORSMiddleware, CORSMiddleware,
allow_origins=allowed_origins, allow_origins=["http://localhost:5173", "http://localhost:3000"],
allow_credentials=True, allow_credentials=True,
allow_methods=["*"], allow_methods=["*"],
allow_headers=["*"], allow_headers=["*"],
@ -72,7 +63,6 @@ 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 [],
@ -95,7 +85,6 @@ 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 [],
@ -116,7 +105,6 @@ 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 [],
@ -149,7 +137,6 @@ 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

@ -1,5 +1,5 @@
fastapi==0.115.0 fastapi==0.103.2
uvicorn[standard]==0.30.1 uvicorn[standard]==0.23.2
pydantic==2.7.4 pydantic==2.7.4
python-dotenv==1.0.1 python-dotenv==1.0.1

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,12 +18,8 @@ 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

@ -1,9 +0,0 @@
node_modules
npm-debug.log
dist
.git
.gitignore
README.md
.env
.env.local
.DS_Store

3
frontend/.gitignore vendored
View File

@ -12,9 +12,6 @@ dist
dist-ssr dist-ssr
*.local *.local
# Local development env.js (production uses runtime injection)
public/env.js
# Editor directories and files # Editor directories and files
.vscode/* .vscode/*
!.vscode/extensions.json !.vscode/extensions.json

View File

@ -1,31 +0,0 @@
#!/bin/sh
# Don't use set -e to avoid crashing if chown fails
# Generate env.js from API_BASE environment variable
# This is set in the Helm deployment values
TARGET="/usr/share/nginx/html/env.js"
# API_BASE should be set via deployment env (e.g., from Helm values)
# Default to /api as fallback (relative path)
: ${API_BASE:=/api}
echo "[ENTRYPOINT] Generating env.js with API_BASE=${API_BASE}"
cat > "$TARGET" <<EOF
window.__ENV__ = {
API_BASE: "${API_BASE}"
};
EOF
if [ -f "$TARGET" ]; then
echo "[ENTRYPOINT] ✓ env.js generated successfully at $TARGET"
cat "$TARGET"
else
echo "[ENTRYPOINT] ✗ Failed to generate env.js"
exit 1
fi
# Ensure ownership/permissions for nginx (don't fail if this doesn't work)
chown nginx:nginx /usr/share/nginx/html/env.js 2>/dev/null || echo "[ENTRYPOINT] Note: Could not change ownership (not critical)"
echo "[ENTRYPOINT] env.js setup complete"

View File

@ -1,37 +0,0 @@
# Build stage
FROM node:22-alpine AS builder
WORKDIR /app
# Copy package files
COPY package*.json ./
# Install dependencies
RUN npm ci --prefer-offline --no-audit
# Copy source code
COPY . .
# Build the application
RUN npm run build
# Production stage - use nginx to serve static files
FROM nginx:alpine
# Copy nginx config
COPY nginx.conf /etc/nginx/nginx.conf
# Copy built app from builder stage
COPY --from=builder /app/dist /usr/share/nginx/html
# Copy entrypoint script to nginx entrypoint.d directory
# This will run before nginx starts and generate env.js from API_BASE env var
COPY 10-generate-env.sh /docker-entrypoint.d/10-generate-env.sh
# Ensure entrypoint script is executable and has correct line endings
RUN chmod +x /docker-entrypoint.d/10-generate-env.sh && \
sed -i 's/\r$//' /docker-entrypoint.d/10-generate-env.sh
EXPOSE 80
# nginx will start automatically; our script in /docker-entrypoint.d runs first

View File

@ -5,8 +5,6 @@
<link rel="icon" type="image/svg+xml" href="/vite.svg" /> <link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>frontend</title> <title>frontend</title>
<!-- Load environment variables before app starts -->
<script src="/env.js"></script>
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>

View File

@ -1,28 +0,0 @@
worker_processes 1;
events { worker_connections 1024; }
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.html;
# Serve static files and fallback to index.html for SPA
location / {
try_files $uri $uri/ /index.html;
}
# Optional caching for static assets
location ~* \.(?:css|js|svg|png|jpg|jpeg|gif|ico)$ {
try_files $uri =404;
expires 7d;
add_header Cache-Control "public";
}
}
}

View File

@ -1,3 +0,0 @@
window.__ENV__ = {
API_BASE: "${API_BASE:-/api}"
};

View File

@ -314,13 +314,6 @@ 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,7 +19,6 @@ 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("");
@ -96,11 +95,6 @@ function App() {
} }
} }
// Filter by made_by
if (filterMadeBy && (!recipe.made_by || recipe.made_by !== filterMadeBy)) {
return false;
}
return true; return true;
}); });
}; };
@ -228,8 +222,6 @@ function App() {
onMaxTimeChange={setFilterMaxTime} onMaxTimeChange={setFilterMaxTime}
filterTags={filterTags} filterTags={filterTags}
onTagsChange={setFilterTags} onTagsChange={setFilterTags}
filterMadeBy={filterMadeBy}
onMadeByChange={setFilterMadeBy}
/> />
</section> </section>

View File

@ -1,52 +1,31 @@
// Get API base from injected env.js or fallback to /api relative path import axios from "axios";
const getApiBase = () => {
if (typeof window !== "undefined" && window.__ENV__ && window.__ENV__.API_BASE) {
return window.__ENV__.API_BASE;
}
// Default to relative /api path for local/containerized deployments
return "/api";
};
const API_BASE = getApiBase(); const API_BASE = "http://localhost:8000";
export async function getRecipes() { export async function getRecipes() {
const res = await fetch(`${API_BASE}/recipes`); const res = await axios.get(`${API_BASE}/recipes`);
if (!res.ok) { return res.data;
throw new Error("Failed to fetch recipes");
}
return res.json();
} }
export async function getRandomRecipe(filters) { export async function getRandomRecipe(filters) {
const params = new URLSearchParams(); const params = {};
if (filters.mealType) params.append("meal_type", filters.mealType); if (filters.mealType) params.meal_type = filters.mealType;
if (filters.maxTime) params.append("max_time", filters.maxTime); if (filters.maxTime) params.max_time = filters.maxTime;
if (filters.ingredients && filters.ingredients.length > 0) { if (filters.ingredients && filters.ingredients.length > 0) {
filters.ingredients.forEach((ing) => { filters.ingredients.forEach((ing) => {
params.append("ingredients", ing); params.ingredients = params.ingredients || [];
params.ingredients.push(ing);
}); });
} }
const queryString = params.toString(); const res = await axios.get(`${API_BASE}/recipes/random`, { params });
const url = queryString ? `${API_BASE}/recipes/random?${queryString}` : `${API_BASE}/recipes/random`; return res.data;
const res = await fetch(url);
if (!res.ok) {
throw new Error("Failed to fetch random recipe");
}
return res.json();
} }
export async function createRecipe(recipe) { export async function createRecipe(recipe) {
const res = await fetch(`${API_BASE}/recipes`, { const res = await axios.post(`${API_BASE}/recipes`, recipe);
method: "POST", return res.data;
headers: { "Content-Type": "application/json" },
body: JSON.stringify(recipe),
});
if (!res.ok) {
throw new Error("Failed to create recipe");
}
return res.json();
} }
export async function updateRecipe(id, payload) { export async function updateRecipe(id, payload) {

View File

@ -26,9 +26,6 @@ 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,7 +4,6 @@ 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("");
@ -19,7 +18,6 @@ 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 || [""]);
@ -28,7 +26,6 @@ function RecipeFormDrawer({ open, onClose, onSubmit, editingRecipe = null }) {
setName(""); setName("");
setMealType("lunch"); setMealType("lunch");
setTimeMinutes(15); setTimeMinutes(15);
setMadeBy("");
setTags(""); setTags("");
setImage(""); setImage("");
setIngredients([""]); setIngredients([""]);
@ -97,10 +94,6 @@ 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;
} }
@ -152,15 +145,6 @@ 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,8 +14,6 @@ function RecipeSearchList({
onMaxTimeChange, onMaxTimeChange,
filterTags, filterTags,
onTagsChange, onTagsChange,
filterMadeBy,
onMadeByChange,
}) { }) {
const [expandFilters, setExpandFilters] = useState(false); const [expandFilters, setExpandFilters] = useState(false);
@ -27,9 +25,6 @@ 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;
@ -47,10 +42,9 @@ function RecipeSearchList({
onMealTypeChange(""); onMealTypeChange("");
onMaxTimeChange(""); onMaxTimeChange("");
onTagsChange([]); onTagsChange([]);
onMadeByChange("");
}; };
const hasActiveFilters = searchQuery || filterMealType || filterMaxTime || filterTags.length > 0 || filterMadeBy; const hasActiveFilters = searchQuery || filterMealType || filterMaxTime || filterTags.length > 0;
return ( return (
<section className="panel secondary recipe-search-list"> <section className="panel secondary recipe-search-list">
@ -158,30 +152,6 @@ 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

@ -4,5 +4,4 @@ import react from '@vitejs/plugin-react'
// https://vite.dev/config/ // https://vite.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [react()], plugins: [react()],
assetsInclude: ['**/*.svg'],
}) })