Files
Cookbook/public/index.html
Johannes 8766d87a3c 1.0
2026-03-12 17:08:00 +01:00

875 lines
28 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>The Recipe Box</title>
<link href="https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,400;0,600;0,700;1,400&family=Source+Sans+3:wght@300;400;500;600&display=swap" rel="stylesheet">
<style>
:root {
--cream: #FDF6E3;
--warm-white: #FFFEF9;
--terracotta: #C45D3A;
--terracotta-dark: #A34A2D;
--olive: #5C6B4A;
--olive-light: #8B9A6D;
--charcoal: #2D2A26;
--warm-gray: #6B6560;
--gold: #D4A853;
--paper: #F9F3E8;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Source Sans 3', sans-serif;
background: var(--cream);
color: var(--charcoal);
min-height: 100vh;
background-image:
radial-gradient(ellipse at 20% 0%, rgba(212, 168, 83, 0.08) 0%, transparent 50%),
radial-gradient(ellipse at 80% 100%, rgba(196, 93, 58, 0.06) 0%, transparent 50%);
}
/* Header */
header {
text-align: center;
padding: 4rem 2rem 3rem;
position: relative;
}
header::after {
content: '✦';
display: block;
font-size: 1.2rem;
color: var(--gold);
margin-top: 1.5rem;
letter-spacing: 1rem;
}
h1 {
font-family: 'Playfair Display', serif;
font-size: clamp(2.5rem, 8vw, 4.5rem);
font-weight: 400;
letter-spacing: -0.02em;
color: var(--charcoal);
}
h1 span {
font-style: italic;
color: var(--terracotta);
}
.subtitle {
font-size: 0.95rem;
color: var(--warm-gray);
margin-top: 0.75rem;
letter-spacing: 0.15em;
text-transform: uppercase;
font-weight: 300;
}
/* Main Layout */
main {
max-width: 1200px;
margin: 0 auto;
padding: 0 2rem 4rem;
}
/* Toolbar */
.toolbar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 3rem;
flex-wrap: wrap;
gap: 1rem;
}
.recipe-count {
font-family: 'Playfair Display', serif;
font-size: 1.1rem;
color: var(--warm-gray);
}
.recipe-count strong {
color: var(--charcoal);
font-weight: 600;
}
.btn {
font-family: 'Source Sans 3', sans-serif;
font-size: 0.9rem;
font-weight: 500;
padding: 0.9rem 2rem;
border: none;
border-radius: 50px;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
letter-spacing: 0.05em;
}
.btn-primary {
background: var(--terracotta);
color: white;
box-shadow: 0 4px 20px rgba(196, 93, 58, 0.25);
}
.btn-primary:hover {
background: var(--terracotta-dark);
transform: translateY(-2px);
box-shadow: 0 6px 25px rgba(196, 93, 58, 0.35);
}
.btn-secondary {
background: transparent;
color: var(--charcoal);
border: 1.5px solid var(--charcoal);
}
.btn-secondary:hover {
background: var(--charcoal);
color: white;
}
.btn-ghost {
background: transparent;
color: var(--warm-gray);
padding: 0.5rem 1rem;
}
.btn-ghost:hover {
color: var(--terracotta);
}
/* Recipe Grid */
.recipes-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 2rem;
}
.recipe-card {
background: var(--warm-white);
border-radius: 1rem;
overflow: hidden;
box-shadow: 0 4px 30px rgba(45, 42, 38, 0.06);
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
animation: fadeInUp 0.5s ease-out backwards;
}
.recipe-card:nth-child(1) { animation-delay: 0.05s; }
.recipe-card:nth-child(2) { animation-delay: 0.1s; }
.recipe-card:nth-child(3) { animation-delay: 0.15s; }
.recipe-card:nth-child(4) { animation-delay: 0.2s; }
.recipe-card:nth-child(5) { animation-delay: 0.25s; }
.recipe-card:nth-child(6) { animation-delay: 0.3s; }
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.recipe-card:hover {
transform: translateY(-6px);
box-shadow: 0 12px 50px rgba(45, 42, 38, 0.12);
}
.card-header {
padding: 2rem 2rem 1.5rem;
background: linear-gradient(135deg, var(--paper) 0%, var(--warm-white) 100%);
border-bottom: 1px dashed rgba(107, 101, 96, 0.15);
position: relative;
}
.card-header::before {
content: '';
position: absolute;
top: 1rem;
right: 1rem;
width: 40px;
height: 40px;
background: url("data:image/svg+xml,%3Csvg viewBox='0 0 40 40' xmlns='http://www.w3.org/2000/svg'%3E%3Ccircle cx='20' cy='20' r='18' fill='none' stroke='%23D4A853' stroke-width='1' opacity='0.3'/%3E%3C/svg%3E");
}
.recipe-category {
font-size: 0.7rem;
text-transform: uppercase;
letter-spacing: 0.2em;
color: var(--olive);
font-weight: 600;
margin-bottom: 0.5rem;
}
.recipe-title {
font-family: 'Playfair Display', serif;
font-size: 1.5rem;
font-weight: 600;
color: var(--charcoal);
line-height: 1.3;
}
.card-body {
padding: 1.5rem 2rem;
}
.recipe-description {
color: var(--warm-gray);
font-size: 0.95rem;
line-height: 1.6;
margin-bottom: 1.25rem;
}
.recipe-meta {
display: flex;
gap: 1.5rem;
font-size: 0.85rem;
color: var(--warm-gray);
}
.recipe-meta span {
display: flex;
align-items: center;
gap: 0.4rem;
}
.recipe-meta svg {
width: 16px;
height: 16px;
opacity: 0.6;
}
.card-footer {
padding: 1rem 2rem 1.5rem;
display: flex;
gap: 0.75rem;
}
/* Empty State */
.empty-state {
text-align: center;
padding: 5rem 2rem;
grid-column: 1 / -1;
}
.empty-state-icon {
font-size: 4rem;
margin-bottom: 1.5rem;
opacity: 0.3;
}
.empty-state h3 {
font-family: 'Playfair Display', serif;
font-size: 1.5rem;
color: var(--charcoal);
margin-bottom: 0.5rem;
}
.empty-state p {
color: var(--warm-gray);
}
/* Modal */
.modal-overlay {
position: fixed;
inset: 0;
background: rgba(45, 42, 38, 0.6);
backdrop-filter: blur(4px);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
opacity: 0;
visibility: hidden;
transition: all 0.3s ease;
padding: 1rem;
}
.modal-overlay.active {
opacity: 1;
visibility: visible;
}
.modal {
background: var(--warm-white);
border-radius: 1.5rem;
max-width: 600px;
width: 100%;
max-height: 90vh;
overflow-y: auto;
transform: scale(0.9) translateY(20px);
transition: transform 0.4s cubic-bezier(0.4, 0, 0.2, 1);
}
.modal-overlay.active .modal {
transform: scale(1) translateY(0);
}
.modal-header {
padding: 2rem 2rem 1rem;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid rgba(107, 101, 96, 0.1);
}
.modal-header h2 {
font-family: 'Playfair Display', serif;
font-size: 1.75rem;
font-weight: 600;
}
.close-btn {
width: 40px;
height: 40px;
border: none;
background: var(--paper);
border-radius: 50%;
cursor: pointer;
font-size: 1.5rem;
color: var(--warm-gray);
transition: all 0.2s;
display: flex;
align-items: center;
justify-content: center;
}
.close-btn:hover {
background: var(--terracotta);
color: white;
}
.modal-body {
padding: 2rem;
}
/* Form Styles */
.form-group {
margin-bottom: 1.75rem;
}
label {
display: block;
font-size: 0.85rem;
font-weight: 500;
color: var(--charcoal);
margin-bottom: 0.5rem;
letter-spacing: 0.03em;
}
input, textarea, select {
width: 100%;
padding: 1rem 1.25rem;
border: 1.5px solid rgba(107, 101, 96, 0.2);
border-radius: 0.75rem;
font-family: 'Source Sans 3', sans-serif;
font-size: 1rem;
background: var(--paper);
transition: all 0.2s;
color: var(--charcoal);
}
input:focus, textarea:focus, select:focus {
outline: none;
border-color: var(--terracotta);
box-shadow: 0 0 0 4px rgba(196, 93, 58, 0.1);
}
textarea {
resize: vertical;
min-height: 100px;
}
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
}
.form-actions {
display: flex;
gap: 1rem;
justify-content: flex-end;
margin-top: 2rem;
padding-top: 1.5rem;
border-top: 1px solid rgba(107, 101, 96, 0.1);
}
/* View Modal */
.view-modal .modal {
max-width: 700px;
}
.view-header {
padding: 2.5rem 2.5rem 2rem;
background: linear-gradient(135deg, var(--paper) 0%, var(--warm-white) 100%);
position: relative;
}
.view-category {
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.2em;
color: var(--olive);
font-weight: 600;
margin-bottom: 0.75rem;
}
.view-title {
font-family: 'Playfair Display', serif;
font-size: 2.25rem;
font-weight: 600;
color: var(--charcoal);
line-height: 1.2;
margin-bottom: 1rem;
}
.view-description {
font-size: 1.1rem;
color: var(--warm-gray);
font-style: italic;
font-family: 'Playfair Display', serif;
}
.view-meta {
display: flex;
gap: 2rem;
padding: 1.5rem 2.5rem;
border-bottom: 1px dashed rgba(107, 101, 96, 0.15);
}
.meta-item {
text-align: center;
}
.meta-value {
font-family: 'Playfair Display', serif;
font-size: 1.5rem;
color: var(--terracotta);
font-weight: 600;
}
.meta-label {
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.15em;
color: var(--warm-gray);
margin-top: 0.25rem;
}
.view-section {
padding: 2rem 2.5rem;
}
.view-section:not(:last-child) {
border-bottom: 1px solid rgba(107, 101, 96, 0.08);
}
.view-section h3 {
font-family: 'Playfair Display', serif;
font-size: 1.25rem;
margin-bottom: 1.25rem;
color: var(--charcoal);
display: flex;
align-items: center;
gap: 0.75rem;
}
.view-section h3::before {
content: '';
width: 24px;
height: 2px;
background: var(--gold);
}
.ingredients-list {
list-style: none;
}
.ingredients-list li {
padding: 0.6rem 0;
border-bottom: 1px dotted rgba(107, 101, 96, 0.15);
color: var(--charcoal);
position: relative;
padding-left: 1.5rem;
}
.ingredients-list li::before {
content: '◇';
position: absolute;
left: 0;
color: var(--olive-light);
font-size: 0.7rem;
}
.instructions-list {
list-style: none;
counter-reset: step;
}
.instructions-list li {
padding: 1rem 0;
padding-left: 3.5rem;
position: relative;
color: var(--charcoal);
line-height: 1.7;
}
.instructions-list li::before {
counter-increment: step;
content: counter(step);
position: absolute;
left: 0;
width: 2rem;
height: 2rem;
background: var(--paper);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-family: 'Playfair Display', serif;
font-weight: 600;
color: var(--terracotta);
font-size: 0.9rem;
}
/* Toast */
.toast {
position: fixed;
bottom: 2rem;
left: 50%;
transform: translateX(-50%) translateY(100px);
background: var(--charcoal);
color: white;
padding: 1rem 2rem;
border-radius: 50px;
font-size: 0.95rem;
opacity: 0;
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
z-index: 2000;
}
.toast.show {
transform: translateX(-50%) translateY(0);
opacity: 1;
}
/* Responsive */
@media (max-width: 640px) {
.form-row {
grid-template-columns: 1fr;
}
.toolbar {
flex-direction: column;
text-align: center;
}
.view-meta {
flex-wrap: wrap;
gap: 1rem;
}
}
</style>
</head>
<body>
<header>
<h1>The <span>Recipe</span> Box</h1>
<p class="subtitle">A collection of culinary treasures</p>
</header>
<main>
<div class="toolbar">
<p class="recipe-count"><strong id="recipeCount">0</strong> recipes in your collection</p>
<button class="btn btn-primary" onclick="openAddModal()">+ Add Recipe</button>
</div>
<div class="recipes-grid" id="recipesGrid">
<!-- Recipes will be loaded here -->
</div>
</main>
<!-- Add/Edit Modal -->
<div class="modal-overlay" id="formModal">
<div class="modal">
<div class="modal-header">
<h2 id="modalTitle">Add New Recipe</h2>
<button class="close-btn" onclick="closeModal('formModal')">&times;</button>
</div>
<div class="modal-body">
<form id="recipeForm" onsubmit="saveRecipe(event)">
<input type="hidden" id="recipeId">
<div class="form-group">
<label for="title">Recipe Title</label>
<input type="text" id="title" placeholder="e.g., Grandma's Apple Pie" required>
</div>
<div class="form-row">
<div class="form-group">
<label for="category">Category</label>
<select id="category">
<option value="Breakfast">Breakfast</option>
<option value="Lunch">Lunch</option>
<option value="Dinner">Dinner</option>
<option value="Dessert">Dessert</option>
<option value="Appetizer">Appetizer</option>
<option value="Snack">Snack</option>
<option value="Beverage">Beverage</option>
</select>
</div>
<div class="form-group">
<label for="servings">Servings</label>
<input type="text" id="servings" placeholder="e.g., 4-6">
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="prepTime">Prep Time</label>
<input type="text" id="prepTime" placeholder="e.g., 20 mins">
</div>
<div class="form-group">
<label for="cookTime">Cook Time</label>
<input type="text" id="cookTime" placeholder="e.g., 45 mins">
</div>
</div>
<div class="form-group">
<label for="description">Description</label>
<textarea id="description" placeholder="A brief description of this dish..."></textarea>
</div>
<div class="form-group">
<label for="ingredients">Ingredients (one per line)</label>
<textarea id="ingredients" rows="6" placeholder="2 cups flour&#10;1 tsp salt&#10;3 eggs" required></textarea>
</div>
<div class="form-group">
<label for="instructions">Instructions (one step per line)</label>
<textarea id="instructions" rows="6" placeholder="Preheat oven to 350°F&#10;Mix dry ingredients&#10;Add wet ingredients" required></textarea>
</div>
<div class="form-actions">
<button type="button" class="btn btn-secondary" onclick="closeModal('formModal')">Cancel</button>
<button type="submit" class="btn btn-primary">Save Recipe</button>
</div>
</form>
</div>
</div>
</div>
<!-- View Modal -->
<div class="modal-overlay view-modal" id="viewModal">
<div class="modal">
<button class="close-btn" onclick="closeModal('viewModal')" style="position:absolute;top:1rem;right:1rem;z-index:10">&times;</button>
<div class="view-header">
<p class="view-category" id="viewCategory"></p>
<h2 class="view-title" id="viewTitle"></h2>
<p class="view-description" id="viewDescription"></p>
</div>
<div class="view-meta" id="viewMeta"></div>
<div class="view-section">
<h3>Ingredients</h3>
<ul class="ingredients-list" id="viewIngredients"></ul>
</div>
<div class="view-section">
<h3>Instructions</h3>
<ol class="instructions-list" id="viewInstructions"></ol>
</div>
<div class="view-section" style="display:flex;gap:1rem;">
<button class="btn btn-secondary" onclick="editCurrentRecipe()">Edit Recipe</button>
<button class="btn btn-ghost" onclick="deleteCurrentRecipe()" style="color:var(--terracotta)">Delete</button>
</div>
</div>
</div>
<!-- Toast -->
<div class="toast" id="toast"></div>
<script>
let recipes = [];
let currentRecipeId = null;
// Load recipes on page load
document.addEventListener('DOMContentLoaded', loadRecipes);
async function loadRecipes() {
try {
const response = await fetch('/api/recipes');
recipes = await response.json();
renderRecipes();
} catch (error) {
showToast('Failed to load recipes');
}
}
function renderRecipes() {
const grid = document.getElementById('recipesGrid');
const count = document.getElementById('recipeCount');
count.textContent = recipes.length;
if (recipes.length === 0) {
grid.innerHTML = `
<div class="empty-state">
<div class="empty-state-icon">📖</div>
<h3>Your recipe box is empty</h3>
<p>Add your first recipe to get started!</p>
</div>
`;
return;
}
grid.innerHTML = recipes.map(recipe => `
<article class="recipe-card" onclick="viewRecipe('${recipe.id}')">
<div class="card-header">
<p class="recipe-category">${recipe.category || 'Uncategorized'}</p>
<h3 class="recipe-title">${recipe.title}</h3>
</div>
<div class="card-body">
<p class="recipe-description">${recipe.description || 'No description provided.'}</p>
<div class="recipe-meta">
${recipe.prepTime ? `<span><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/></svg>${recipe.prepTime}</span>` : ''}
${recipe.servings ? `<span><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>${recipe.servings}</span>` : ''}
</div>
</div>
</article>
`).join('');
}
function openAddModal() {
document.getElementById('modalTitle').textContent = 'Add New Recipe';
document.getElementById('recipeForm').reset();
document.getElementById('recipeId').value = '';
openModal('formModal');
}
function openModal(id) {
document.getElementById(id).classList.add('active');
document.body.style.overflow = 'hidden';
}
function closeModal(id) {
document.getElementById(id).classList.remove('active');
document.body.style.overflow = '';
}
async function saveRecipe(event) {
event.preventDefault();
const id = document.getElementById('recipeId').value;
const recipe = {
title: document.getElementById('title').value,
category: document.getElementById('category').value,
servings: document.getElementById('servings').value,
prepTime: document.getElementById('prepTime').value,
cookTime: document.getElementById('cookTime').value,
description: document.getElementById('description').value,
ingredients: document.getElementById('ingredients').value.split('\n').filter(i => i.trim()),
instructions: document.getElementById('instructions').value.split('\n').filter(i => i.trim())
};
try {
const url = id ? `/api/recipes/${id}` : '/api/recipes';
const method = id ? 'PUT' : 'POST';
const response = await fetch(url, {
method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(recipe)
});
if (response.ok) {
closeModal('formModal');
await loadRecipes();
showToast(id ? 'Recipe updated!' : 'Recipe saved!');
}
} catch (error) {
showToast('Failed to save recipe');
}
}
function viewRecipe(id) {
const recipe = recipes.find(r => r.id === id);
if (!recipe) return;
currentRecipeId = id;
document.getElementById('viewCategory').textContent = recipe.category || 'Uncategorized';
document.getElementById('viewTitle').textContent = recipe.title;
document.getElementById('viewDescription').textContent = recipe.description || '';
// Meta
let metaHtml = '';
if (recipe.prepTime) metaHtml += `<div class="meta-item"><p class="meta-value">${recipe.prepTime}</p><p class="meta-label">Prep Time</p></div>`;
if (recipe.cookTime) metaHtml += `<div class="meta-item"><p class="meta-value">${recipe.cookTime}</p><p class="meta-label">Cook Time</p></div>`;
if (recipe.servings) metaHtml += `<div class="meta-item"><p class="meta-value">${recipe.servings}</p><p class="meta-label">Servings</p></div>`;
document.getElementById('viewMeta').innerHTML = metaHtml;
// Ingredients
document.getElementById('viewIngredients').innerHTML =
(recipe.ingredients || []).map(i => `<li>${i}</li>`).join('');
// Instructions
document.getElementById('viewInstructions').innerHTML =
(recipe.instructions || []).map(i => `<li>${i}</li>`).join('');
openModal('viewModal');
}
function editCurrentRecipe() {
const recipe = recipes.find(r => r.id === currentRecipeId);
if (!recipe) return;
closeModal('viewModal');
document.getElementById('modalTitle').textContent = 'Edit Recipe';
document.getElementById('recipeId').value = recipe.id;
document.getElementById('title').value = recipe.title;
document.getElementById('category').value = recipe.category || 'Dinner';
document.getElementById('servings').value = recipe.servings || '';
document.getElementById('prepTime').value = recipe.prepTime || '';
document.getElementById('cookTime').value = recipe.cookTime || '';
document.getElementById('description').value = recipe.description || '';
document.getElementById('ingredients').value = (recipe.ingredients || []).join('\n');
document.getElementById('instructions').value = (recipe.instructions || []).join('\n');
openModal('formModal');
}
async function deleteCurrentRecipe() {
if (!confirm('Are you sure you want to delete this recipe?')) return;
try {
const response = await fetch(`/api/recipes/${currentRecipeId}`, { method: 'DELETE' });
if (response.ok) {
closeModal('viewModal');
await loadRecipes();
showToast('Recipe deleted');
}
} catch (error) {
showToast('Failed to delete recipe');
}
}
function showToast(message) {
const toast = document.getElementById('toast');
toast.textContent = message;
toast.classList.add('show');
setTimeout(() => toast.classList.remove('show'), 3000);
}
</script>
</body>
</html>