Add favorites feature - toggle, list, and pre-fill on add

This commit is contained in:
Otto
2026-03-30 17:00:21 -04:00
parent 45e988cbe5
commit 3ac828bea3
16 changed files with 3174 additions and 32 deletions

View File

@@ -1,9 +1,11 @@
import { useState, useEffect } from 'react';
import { Link, useSearchParams } from 'react-router-dom';
export default function Layout({ children }) {
export default function Layout({ children, showFavorites = false }) {
const [darkMode, setDarkMode] = useState(true);
const [selectedDate, setSelectedDate] = useState(new Date().toISOString().split('T')[0]);
const [favorites, setFavorites] = useState([]);
const [showFavList, setShowFavList] = useState(false);
const [searchParams] = useSearchParams();
const returnDate = searchParams.get('date');
@@ -21,6 +23,13 @@ export default function Layout({ children }) {
}
}, [darkMode]);
useEffect(() => {
fetch('/api/favorites')
.then(res => res.json())
.then(data => setFavorites(data))
.catch(console.error);
}, [showFavList]);
function handleDateChange(e) {
setSelectedDate(e.target.value);
}
@@ -29,6 +38,19 @@ export default function Layout({ children }) {
setSelectedDate(new Date().toISOString().split('T')[0]);
}
// Build query string for adding a favorite
function buildFavoriteUrl(fav) {
const params = new URLSearchParams();
params.set('name', fav.name);
if (fav.description) params.set('description', fav.description);
if (fav.calories) params.set('calories', fav.calories);
if (fav.protein) params.set('protein', fav.protein);
if (fav.carbs) params.set('carbs', fav.carbs);
if (fav.fat) params.set('fat', fav.fat);
if (fav.notes) params.set('notes', fav.notes);
return '/add?' + params.toString();
}
const bgMain = darkMode ? 'bg-gray-900' : 'bg-gray-100';
const bgCard = darkMode ? 'bg-gray-800' : 'bg-white';
const textMain = darkMode ? 'text-white' : 'text-gray-900';
@@ -52,6 +74,13 @@ export default function Layout({ children }) {
Today
</button>
)}
<button
onClick={() => setShowFavList(!showFavList)}
className="text-2xl"
title="Favorites"
>
</button>
</div>
<button
onClick={() => setDarkMode(!darkMode)}
@@ -60,6 +89,29 @@ export default function Layout({ children }) {
{darkMode ? '☀️' : '🌙'}
</button>
</div>
{/* Favorites Dropdown */}
{showFavList && (
<div className={bgCard + " mx-4 mt-2 p-3 rounded-xl shadow-lg absolute z-50 w-[calc(100%-2rem)] max-w-md"}>
<div className={"font-semibold mb-2 " + textMain}>Add from Favorites</div>
{favorites.length === 0 ? (
<p className={textMuted}>No favorites yet</p>
) : (
<div className="space-y-2 max-h-60 overflow-y-auto">
{favorites.map(fav => (
<Link
key={fav.id}
to={buildFavoriteUrl(fav)}
onClick={() => setShowFavList(false)}
className={"block p-2 rounded hover:bg-gray-700 " + textMuted}
>
<span className={textMain}>{fav.name}</span> {fav.calories} cal
</Link>
))}
</div>
)}
</div>
)}
<div className="p-4 max-w-md mx-auto">
{children}

View File

@@ -1,9 +1,10 @@
import { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { useNavigate, useSearchParams } from 'react-router-dom';
import Layout from '../components/Layout';
export default function AddEntry() {
const navigate = useNavigate();
const [searchParams] = useSearchParams();
const [darkMode, setDarkMode] = useState(true);
const [foods, setFoods] = useState([]);
const [searchQuery, setSearchQuery] = useState('');
@@ -25,6 +26,24 @@ export default function AddEntry() {
setDarkMode(document.documentElement.classList.contains('dark'));
}, []);
// Pre-fill from query params (favorites)
useEffect(() => {
const name = searchParams.get('name');
if (name) {
setForm({
...form,
name: name,
description: searchParams.get('description') || '',
notes: searchParams.get('notes') || '',
calories: searchParams.get('calories') || '',
protein: searchParams.get('protein') || '',
carbs: searchParams.get('carbs') || '',
fat: searchParams.get('fat') || '',
image: null
});
}
}, [searchParams]);
const bgCard = darkMode ? 'bg-gray-800' : 'bg-white';
const textMain = darkMode ? 'text-white' : 'text-gray-900';
const textMuted = darkMode ? 'text-gray-400' : 'text-gray-500';

View File

@@ -36,6 +36,16 @@ export default function EntryDetail() {
window.location.href = returnDate ? '/?date=' + returnDate : '/';
}
async function toggleFavorite() {
try {
const res = await fetch('/api/entries/' + id + '/favorite', { method: 'POST' });
const data = await res.json();
setEntry({ ...entry, favorite: data.favorite });
} catch (err) {
console.error(err);
}
}
const bgCard = darkMode ? 'bg-gray-800' : 'bg-white';
const textMain = darkMode ? 'text-white' : 'text-gray-900';
const textMuted = darkMode ? 'text-gray-400' : 'text-gray-500';
@@ -49,7 +59,17 @@ export default function EntryDetail() {
<Layout>
<Link to={returnUrl} className="text-emerald-500 mb-4 block"> Back</Link>
<h1 className={"text-2xl font-bold mb-2 " + textMain}>{entry.name}</h1>
<div className="flex items-center gap-2">
<h1 className={"text-2xl font-bold mb-2 flex-1 " + textMain}>{entry.name}</h1>
<button
onClick={toggleFavorite}
className="text-3xl"
title={entry.favorite ? "Remove from favorites" : "Add to favorites"}
>
{entry.favorite ? '⭐' : '☆'}
</button>
</div>
{entry.description && <p className={textMuted + " mb-4"}>{entry.description}</p>}
{entry.image_url && (
@@ -89,9 +109,11 @@ export default function EntryDetail() {
</div>
)}
<button onClick={handleDelete} className="w-full bg-red-500 hover:bg-red-600 text-white py-3 rounded-xl font-semibold">
Delete Entry
</button>
<div className="flex gap-2">
<button onClick={handleDelete} className="flex-1 bg-red-500 hover:bg-red-600 text-white py-3 rounded-xl font-semibold">
Delete Entry
</button>
</div>
</Layout>
);
}