272 lines
9.0 KiB
JavaScript
272 lines
9.0 KiB
JavaScript
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;
|