Last Used implementiert

This commit is contained in:
2025-09-25 19:52:55 +00:00
parent 0bfb8b2074
commit db431553b9
11 changed files with 270 additions and 7 deletions

View File

@@ -633,6 +633,42 @@
padding: 40px;
}
}
/* --- Cook Date Modal (appended) --- */
.modal-overlay {
position: fixed;
inset: 0;
background: rgba(0,0,0,0.45);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.modal-dialog {
background: #fff;
padding: 24px 28px;
border-radius: 12px;
width: 100%;
max-width: 420px;
box-shadow: 0 10px 30px -5px rgba(0,0,0,0.35);
animation: fadeIn 0.18s ease-out;
}
.modal-dialog h3 { margin: 0 0 12px; font-size: 1.25rem; }
.modal-field-group { margin-bottom: 16px; }
.modal-field-group label { display:block; font-size: 0.75rem; letter-spacing: .5px; font-weight:600; margin-bottom:4px; color:#555; text-transform: uppercase; }
.modal-field-group input { width: 100%; padding: 8px 10px; border: 1px solid #cbd5e0; border-radius: 6px; font-size: 0.95rem; box-sizing: border-box; }
.modal-field-inline { display: flex; gap: 10px; }
.modal-field-inline .modal-field-group { flex: 1; margin-bottom: 0; }
.modal-actions { display:flex; justify-content:flex-end; gap:10px; margin-top: 18px; }
.modal-actions button { padding:8px 14px; border:none; border-radius:6px; cursor:pointer; font-weight:500; font-size: 0.9rem; }
.modal-actions .primary { background:#2f855a; color:#fff; }
.modal-actions .primary:hover { background:#276749; }
.modal-actions .secondary { background:#e2e8f0; }
.modal-actions .secondary:hover { background:#cbd5e0; }
.modal-actions .danger { background:#dc3545; color:#fff; }
.modal-actions .danger:hover { background:#c82333; }
@media (max-width: 768px) {
.recipe-detail {

View File

@@ -36,6 +36,9 @@ const RecipeDetail: React.FC = () => {
const [error, setError] = useState<string | null>(null);
const [editingImages, setEditingImages] = useState(false);
const [uploadProgress, setUploadProgress] = useState(0);
const [markingCooked, setMarkingCooked] = useState(false);
const [showCookModal, setShowCookModal] = useState(false);
const [cookDate, setCookDate] = useState<string>(''); // YYYY-MM-DD
useEffect(() => {
const loadRecipe = async () => {
@@ -124,6 +127,43 @@ const RecipeDetail: React.FC = () => {
}
};
const openCookModal = () => {
if (!recipe) return;
const now = new Date();
const isoDate = now.toISOString().slice(0,10); // YYYY-MM-DD
setCookDate(isoDate);
setShowCookModal(true);
};
const submitCooked = async () => {
if (!recipe) return;
if (cookDate) {
const d = new Date(`${cookDate}T00:00:00`);
if (d.getTime() > Date.now() + 60_000) {
setError('Datum liegt in der Zukunft');
return;
}
}
try {
setMarkingCooked(true);
let iso: string | undefined = undefined;
if (cookDate) {
const combined = new Date(`${cookDate}T00:00:00`);
if (!isNaN(combined.getTime())) iso = combined.toISOString();
}
const resp = await recipeApi.markRecipeCooked(recipe.id, iso);
if (resp.success) {
setRecipe({ ...recipe, lastUsed: resp.data.lastUsed || undefined });
setShowCookModal(false);
}
} catch (e) {
console.error('Mark cooked failed', e);
setError('Fehler beim Speichern des Datums');
} finally {
setMarkingCooked(false);
}
};
if (loading) {
return (
<div className="recipe-detail">
@@ -192,6 +232,14 @@ const RecipeDetail: React.FC = () => {
>
📸 {editingImages ? 'Fertig' : 'Bilder verwalten'}
</button>
<button
onClick={openCookModal}
className="edit-button"
title="Datum der letzten Zubereitung setzen"
style={{ marginRight: '8px', background: '#2f855a' }}
>
🍳 {recipe.lastUsed ? 'Zubereitet ändern' : 'Zubereitet'}
</button>
<Link to={`/recipes/${recipe.id}/edit`} className="edit-button">
Bearbeiten
</Link>
@@ -200,6 +248,24 @@ const RecipeDetail: React.FC = () => {
</button>
</div>
</div>
{showCookModal && (
<div className="modal-overlay" role="dialog" aria-modal="true">
<div className="modal-dialog">
<h3>Datum der Zubereitung</h3>
<div className="modal-field-group">
<label htmlFor="cook-date">Datum</label>
<input id="cook-date" type="date" value={cookDate} onChange={e => setCookDate(e.target.value)} />
</div>
<div className="modal-actions">
{recipe.lastUsed && (
<button className="danger" disabled={markingCooked} onClick={async () => { try { setMarkingCooked(true); const resp = await recipeApi.markRecipeCooked(recipe.id, undefined); if (resp.success) { setRecipe({ ...recipe, lastUsed: resp.data.lastUsed || undefined }); } } finally { setMarkingCooked(false); setShowCookModal(false);} }}>Jetzt setzen</button>
)}
<button className="secondary" onClick={() => setShowCookModal(false)} disabled={markingCooked}>Abbrechen</button>
<button className="primary" onClick={submitCooked} disabled={markingCooked}>{markingCooked ? 'Speichere...' : 'Speichern'}</button>
</div>
</div>
</div>
)}
{/* Main Content */}
<div className="recipe-content">
@@ -249,6 +315,14 @@ const RecipeDetail: React.FC = () => {
<span className="meta-label">Portionen:</span>
<span className="meta-value">👥 {recipe.servings}</span>
</div>
{recipe.lastUsed && (
<div className="meta-item">
<span className="meta-label">Zuletzt zubereitet:</span>
<span className="meta-value">
{new Date(recipe.lastUsed).toLocaleDateString('de-DE', { year: 'numeric', month: '2-digit', day: '2-digit' })}
</span>
</div>
)}
</div>
</div>

View File

@@ -198,6 +198,29 @@
gap: 0.25rem;
}
.recipe-lastused {
background: #2f855a;
color: #fff;
padding: 0.25rem 0.6rem;
border-radius: 16px;
font-size: 0.7rem;
font-weight: 600;
letter-spacing: .5px;
}
.recipe-never {
background: #e53e3e;
color: #fff;
padding: 0.25rem 0.6rem;
border-radius: 16px;
font-size: 0.7rem;
font-weight: 600;
letter-spacing: .5px;
display: inline-flex;
align-items: center;
gap: 0.25rem;
}
.recipe-actions {
display: flex;
gap: 0.75rem;

View File

@@ -29,6 +29,8 @@ const RecipeList: React.FC = () => {
const [search, setSearch] = useState('');
const [category, setCategory] = useState('');
const [page, setPage] = useState(1);
const [sortBy, setSortBy] = useState<'title' | 'lastUsed'>('title');
const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('asc');
const [pagination, setPagination] = useState({
page: 1,
limit: 10,
@@ -50,8 +52,8 @@ const RecipeList: React.FC = () => {
category: category || undefined,
page,
limit: 12,
sortBy: 'title',
sortOrder: 'asc',
sortBy,
sortOrder,
});
if (response.success) {
@@ -71,7 +73,7 @@ const RecipeList: React.FC = () => {
useEffect(() => {
loadRecipes();
}, [debouncedSearch, category, page]);
}, [debouncedSearch, category, page, sortBy, sortOrder]);
const handleSearchSubmit = (e: React.FormEvent) => {
e.preventDefault();
@@ -140,6 +142,24 @@ const RecipeList: React.FC = () => {
<option value="Fisch">Fisch</option>
<option value="Dessert">Dessert</option>
</select>
<select
value={sortBy}
onChange={e => { setSortBy(e.target.value as any); setPage(1); }}
className="category-select"
style={{ marginLeft: '8px' }}
>
<option value="title">Titel</option>
<option value="lastUsed">Zuletzt zubereitet</option>
</select>
<button
onClick={() => { setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc'); setPage(1); }}
className="search-button"
style={{ marginLeft: '4px' }}
title="Sortierreihenfolge umschalten"
>
{sortOrder === 'asc' ? '⬆️' : '⬇️'}
</button>
</div>
</div>
@@ -188,6 +208,13 @@ const RecipeList: React.FC = () => {
<span className="recipe-category">{recipe.category}</span>
)}
<span className="recipe-servings">👥 {recipe.servings} Portionen</span>
{recipe.lastUsed ? (
<span className="recipe-lastused" title={new Date(recipe.lastUsed).toLocaleString('de-DE')}>
🍳 {new Date(recipe.lastUsed).toLocaleDateString('de-DE')}
</span>
) : (
<span className="recipe-never" title="Noch nie zubereitet">🆕 Nie</span>
)}
</div>
<div className="recipe-actions">

View File

@@ -37,6 +37,7 @@ export interface Recipe {
ingredients?: string;
instructions?: string;
comment?: string;
lastUsed?: string; // ISO Datum vom Backend
images?: RecipeImage[];
ingredientsList?: Ingredient[];
}
@@ -106,6 +107,12 @@ export const recipeApi = {
const response = await api.delete(`/recipes/${id}`);
return response.data;
},
// Mark recipe as cooked (updates lastUsed)
markRecipeCooked: async (id: number, lastUsed?: string): Promise<ApiResponse<{ id: number; lastUsed: string | null }>> => {
const response = await api.post(`/recipes/${id}/cooked`, lastUsed ? { lastUsed } : {});
return response.data;
},
};
// Ingredient API methods