import { useEffect, useState } from "react"; import "./App.css"; import TopBar from "./components/TopBar"; import RecipeSearchList from "./components/RecipeSearchList"; import RecipeDetails from "./components/RecipeDetails"; import RecipeFormDrawer from "./components/RecipeFormDrawer"; import GroceryLists from "./components/GroceryLists"; import PinnedGroceryLists from "./components/PinnedGroceryLists"; import Modal from "./components/Modal"; import ToastContainer from "./components/ToastContainer"; import ThemeToggle from "./components/ThemeToggle"; import Login from "./components/Login"; import Register from "./components/Register"; import ResetPassword from "./components/ResetPassword"; import { getRecipes, getRandomRecipe, createRecipe, updateRecipe, deleteRecipe } from "./api"; import { getToken, removeToken, getMe } from "./authApi"; function App() { const [isAuthenticated, setIsAuthenticated] = useState(false); const [user, setUser] = useState(null); const [authView, setAuthView] = useState("login"); // "login" or "register" const [loadingAuth, setLoadingAuth] = useState(true); const [resetToken, setResetToken] = useState(null); const [currentView, setCurrentView] = useState(() => { try { return localStorage.getItem("currentView") || "recipes"; } catch { return "recipes"; } }); // "recipes" or "grocery-lists" const [recipes, setRecipes] = useState([]); const [selectedRecipe, setSelectedRecipe] = useState(null); // Recipe listing filters const [searchQuery, setSearchQuery] = useState(""); const [filterMealType, setFilterMealType] = useState(""); const [filterMaxTime, setFilterMaxTime] = useState(""); const [filterTags, setFilterTags] = useState([]); const [filterOwner, setFilterOwner] = useState(""); // Random recipe filters const [mealTypeFilter, setMealTypeFilter] = useState(""); const [maxTimeFilter, setMaxTimeFilter] = useState(""); const [ingredientsFilter, setIngredientsFilter] = useState(""); const [loadingRandom, setLoadingRandom] = useState(false); const [error, setError] = useState(""); const [drawerOpen, setDrawerOpen] = useState(false); const [editingRecipe, setEditingRecipe] = useState(null); const [deleteModal, setDeleteModal] = useState({ isOpen: false, recipeId: null, recipeName: "" }); const [logoutModal, setLogoutModal] = useState(false); const [toasts, setToasts] = useState([]); const [theme, setTheme] = useState(() => { try { return localStorage.getItem("theme") || "dark"; } catch { return "dark"; } }); const [showPinnedSidebar, setShowPinnedSidebar] = useState(false); // Swipe gesture handling for mobile sidebar const [touchStart, setTouchStart] = useState(null); const [touchEnd, setTouchEnd] = useState(null); // Minimum swipe distance (in px) const minSwipeDistance = 50; const onTouchStart = (e) => { setTouchEnd(null); setTouchStart(e.targetTouches[0].clientX); }; const onTouchMove = (e) => { setTouchEnd(e.targetTouches[0].clientX); }; const onTouchEnd = () => { if (!touchStart || !touchEnd) return; const distance = touchStart - touchEnd; const isLeftSwipe = distance > minSwipeDistance; if (isLeftSwipe) { setShowPinnedSidebar(false); } setTouchStart(null); setTouchEnd(null); }; // Check authentication on mount useEffect(() => { const checkAuth = async () => { // Check for reset token in URL const urlParams = new URLSearchParams(window.location.search); const resetTokenParam = urlParams.get('reset_token'); if (resetTokenParam) { setResetToken(resetTokenParam); // Clean URL window.history.replaceState({}, document.title, window.location.pathname); setLoadingAuth(false); return; } const token = getToken(); if (token) { try { const userData = await getMe(token); setUser(userData); setIsAuthenticated(true); } catch (err) { // Only remove token on authentication errors (401), not network errors if (err.status === 401) { console.log("Token invalid or expired, logging out"); removeToken(); setIsAuthenticated(false); } else { // Network error or server error - keep user logged in console.warn("Auth check failed but keeping session:", err.message); setIsAuthenticated(true); // Assume still authenticated } } } setLoadingAuth(false); }; checkAuth(); }, []); // Save currentView to localStorage useEffect(() => { try { localStorage.setItem("currentView", currentView); } catch (err) { console.error("Unable to save view", err); } }, [currentView]); // Load recipes for everyone (readonly for non-authenticated) useEffect(() => { loadRecipes(); }, []); useEffect(() => { document.documentElement.setAttribute("data-theme", theme); try { localStorage.setItem("theme", theme); } catch {} }, [theme]); const loadRecipes = async () => { try { const list = await getRecipes(); setRecipes(list); if (!selectedRecipe && list.length > 0) { setSelectedRecipe(list[0]); } } catch { setError("לא הצלחנו לטעון את רשימת המתכונים."); } }; const getFilteredRecipes = () => { return recipes.filter((recipe) => { // Search by name if (searchQuery && !recipe.name.toLowerCase().includes(searchQuery.toLowerCase())) { return false; } // Filter by meal type if (filterMealType && recipe.meal_type !== filterMealType) { return false; } // Filter by prep time if (filterMaxTime) { const maxTime = parseInt(filterMaxTime, 10); if (recipe.time_minutes > maxTime) { return false; } } // Filter by tags if (filterTags.length > 0) { const recipeTags = recipe.tags || []; const hasAllTags = filterTags.every((tag) => recipeTags.some((t) => t.toLowerCase() === tag.toLowerCase()) ); if (!hasAllTags) { return false; } } // Filter by made_by (username) if (filterOwner && (!recipe.made_by || recipe.made_by !== filterOwner)) { return false; } return true; }); }; const handleRandomClick = async () => { setLoadingRandom(true); setError(""); try { const ingredientsArr = ingredientsFilter .split(",") .map((s) => s.trim()) .filter(Boolean); const recipe = await getRandomRecipe({ mealType: mealTypeFilter || undefined, maxTime: maxTimeFilter ? Number(maxTimeFilter) : undefined, ingredients: ingredientsArr, }); setSelectedRecipe(recipe); } catch (err) { if (err.response?.status === 404) { setError("לא נמצאו מתכונים שעומדים בפילטרים שלך."); } else { setError("אירעה שגיאה בחיפוש מתכון."); } } finally { setLoadingRandom(false); } }; const handleCreateRecipe = async (payload) => { try { const token = getToken(); const created = await createRecipe(payload, token); setDrawerOpen(false); setEditingRecipe(null); await loadRecipes(); setSelectedRecipe(created); addToast("המתכון החדש נוצר בהצלחה!", "success"); } catch { setError("שגיאה בשמירת המתכון החדש."); addToast("שגיאה בשמירת המתכון החדש", "error"); } }; const handleEditRecipe = (recipe) => { setEditingRecipe(recipe); setDrawerOpen(true); }; const handleUpdateRecipe = async (payload) => { try { const token = getToken(); await updateRecipe(editingRecipe.id, payload, token); setDrawerOpen(false); setEditingRecipe(null); await loadRecipes(); const updated = (await getRecipes()).find((r) => r.id === editingRecipe.id); if (updated) { setSelectedRecipe(updated); } addToast("המתכון עודכן בהצלחה!", "success"); } catch { setError("שגיאה בעדכון המתכון."); addToast("שגיאה בעדכון המתכון", "error"); } }; const handleShowDeleteModal = (recipeId, recipeName) => { setDeleteModal({ isOpen: true, recipeId, recipeName }); }; const handleConfirmDelete = async () => { const recipeId = deleteModal.recipeId; setDeleteModal({ isOpen: false, recipeId: null, recipeName: "" }); try { const token = getToken(); await deleteRecipe(recipeId, token); await loadRecipes(); setSelectedRecipe(null); addToast("המתכון נמחק בהצלחה!", "success"); } catch { setError("שגיאה במחיקת המתכון."); addToast("שגיאה במחיקת המתכון", "error"); } }; const handleCancelDelete = () => { setDeleteModal({ isOpen: false, recipeId: null, recipeName: "" }); }; const addToast = (message, type = "info", duration = 3000) => { const id = Date.now(); setToasts((prev) => [...prev, { id, message, type, duration }]); }; const removeToast = (id) => { setToasts((prev) => prev.filter((toast) => toast.id !== id)); }; const handleFormSubmit = (payload) => { if (editingRecipe) { handleUpdateRecipe(payload); } else { handleCreateRecipe(payload); } }; const handleLoginSuccess = async () => { const token = getToken(); const userData = await getMe(token); setUser(userData); setIsAuthenticated(true); await loadRecipes(); }; const handleLogout = () => { setLogoutModal(true); }; const confirmLogout = () => { removeToken(); setUser(null); setIsAuthenticated(false); setRecipes([]); setSelectedRecipe(null); setLogoutModal(false); addToast('התנתקת בהצלחה', 'success'); }; // Show loading state while checking auth if (loadingAuth) { return (