my-recipes/frontend/src/components/RecipeSearchList.jsx
2025-12-20 22:51:57 +02:00

272 lines
9.0 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useState, useEffect } from "react";
import placeholderLight from "../assets/placeholder-light.png";
import placeholderDark from "../assets/placeholder-dark.png";
function RecipeSearchList({
allRecipes,
recipes,
selectedId,
onSelect,
searchQuery,
onSearchChange,
filterMealType,
onMealTypeChange,
filterMaxTime,
onMaxTimeChange,
filterTags,
onTagsChange,
filterOwner,
onOwnerChange,
}) {
const [expandFilters, setExpandFilters] = useState(false);
const [theme, setTheme] = useState(document.documentElement.getAttribute('data-theme') || 'dark');
useEffect(() => {
const observer = new MutationObserver(() => {
setTheme(document.documentElement.getAttribute('data-theme') || 'dark');
});
observer.observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme'] });
return () => observer.disconnect();
}, []);
const placeholderImage = theme === 'dark' ? placeholderDark : placeholderLight;
// Extract unique tags from ALL recipes (not filtered)
const allTags = Array.from(
new Set(allRecipes.flatMap((recipe) => recipe.tags || []))
).sort();
// Extract unique meal types from ALL recipes (not filtered)
const mealTypes = Array.from(new Set(allRecipes.map((r) => r.meal_type))).sort();
// Extract unique made_by values from ALL recipes
// The made_by field is what the user defined when creating the recipe,
// so we use it for both filtering and display
const madeByMap = new Map();
allRecipes.forEach((r) => {
if (r.made_by) {
// Always use made_by as the display name (it's the custom name the user entered)
if (!madeByMap.has(r.made_by)) {
madeByMap.set(r.made_by, r.made_by);
}
}
});
const allMadeBy = Array.from(madeByMap.keys()).sort();
// 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 sliderMax = maxTimeInRecipes > 0 ? maxTimeInRecipes + 10 : 70;
const handleTagToggle = (tag) => {
if (filterTags.includes(tag)) {
onTagsChange(filterTags.filter((t) => t !== tag));
} else {
onTagsChange([...filterTags, tag]);
}
};
const clearAllFilters = () => {
onSearchChange("");
onMealTypeChange("");
onMaxTimeChange("");
onTagsChange([]);
onOwnerChange("");
};
const hasActiveFilters = searchQuery || filterMealType || filterMaxTime || filterTags.length > 0 || filterOwner;
return (
<section className="panel secondary recipe-search-list">
{/* Header with Search */}
<div className="recipe-search-header">
<div className="search-box-wrapper">
<input
type="text"
className="search-box"
placeholder="חיפוש לפי שם מתכון..."
value={searchQuery}
onChange={(e) => onSearchChange(e.target.value)}
/>
{searchQuery && (
<button
className="search-clear"
onClick={() => onSearchChange("")}
title="נקה חיפוש"
>
</button>
)}
</div>
{/* Filter Toggle Button */}
<button
className="filter-toggle-btn"
onClick={() => setExpandFilters(!expandFilters)}
title={expandFilters ? "הסתר סינונים" : "הצג סינונים"}
>
<span className="filter-icon"></span>
{hasActiveFilters && <span className="filter-badge">{Object.values([filterMealType, filterMaxTime, filterTags.length > 0 ? "tags" : null]).filter(Boolean).length}</span>}
</button>
</div>
{/* Expandable Filters */}
{expandFilters && (
<div className="recipe-filters-expanded">
<div className="filters-grid">
{/* Meal Type Filter */}
{mealTypes.length > 0 && (
<div className="filter-group">
<label className="filter-label">סוג ארוחה</label>
<div className="filter-options">
<button
className={`filter-btn ${filterMealType === "" ? "active" : ""}`}
onClick={() => onMealTypeChange("")}
>
הכל
</button>
{mealTypes.map((type) => (
<button
key={type}
className={`filter-btn ${filterMealType === type ? "active" : ""}`}
onClick={() => onMealTypeChange(type)}
>
{translateMealType(type)}
</button>
))}
</div>
</div>
)}
{/* Prep Time Filter */}
{maxTimeInRecipes > 0 && (
<div className="filter-group">
<label className="filter-label">
זמן הכנה {filterMaxTime ? `עד ${filterMaxTime} דקות` : "הכל"}
</label>
<div className="time-filter-wrapper">
<input
type="range"
min="0"
max={sliderMax}
step="1"
value={filterMaxTime || 0}
onChange={(e) => {
const val = parseInt(e.target.value, 10);
onMaxTimeChange(val === 0 ? "" : String(val));
}}
className="time-slider"
/>
<div className="time-labels">
<span>0</span>
<span>{sliderMax}</span>
</div>
</div>
</div>
)}
{/* Tags Filter */}
{allTags.length > 0 && (
<div className="filter-group">
<label className="filter-label">תגיות</label>
<div className="tag-filter-wrapper">
{allTags.map((tag) => (
<button
key={tag}
className={`tag-filter-btn ${filterTags.includes(tag) ? "active" : ""}`}
onClick={() => handleTagToggle(tag)}
>
{tag}
</button>
))}
</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 ${filterOwner === "" ? "active" : ""}`}
onClick={() => onOwnerChange("")}
>
הכל
</button>
{allMadeBy.map((madeBy) => (
<button
key={madeBy}
className={`filter-btn ${filterOwner === madeBy ? "active" : ""}`}
onClick={() => onOwnerChange(madeBy)}
>
{madeByMap.get(madeBy) || madeBy}
</button>
))}
</div>
</div>
)}
</div>
{/* Clear All Button */}
{hasActiveFilters && (
<button className="btn ghost full" onClick={clearAllFilters} style={{ marginTop: "0.6rem" }}>
נקה הכל
</button>
)}
</div>
)}
{/* Recipe List Header */}
<div className="recipe-list-header">
<h3>כל המתכונים</h3>
<span className="badge">{recipes.length}</span>
</div>
{/* Recipe List */}
{recipes.length === 0 ? (
<p className="muted">עדיין אין מתכונים שתואמים את הסינונים שלך.</p>
) : (
<ul className="recipe-list">
{recipes.map((r) => (
<li
key={r.id}
className={
selectedId === r.id ? "recipe-list-item active" : "recipe-list-item"
}
onClick={() => onSelect(r)}
>
<div className="recipe-list-image">
<img src={r.image || placeholderImage} alt={r.name} />
</div>
<div className="recipe-list-main">
<div className="recipe-list-name">{r.name}</div>
<div className="recipe-list-meta">
<span>{r.time_minutes} דק׳</span>
<span>{translateMealType(r.meal_type)}</span>
</div>
</div>
</li>
))}
</ul>
)}
</section>
);
}
function translateMealType(type) {
switch (type) {
case "breakfast":
return "בוקר";
case "lunch":
return "צהריים";
case "dinner":
return "ערב";
case "snack":
return "קינוחים";
default:
return type;
}
}
export default RecipeSearchList;