Updates
This commit is contained in:
@@ -3,6 +3,8 @@
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
<meta name="theme-color" content="#2563eb" />
|
||||
<title>TikTok Rezepte</title>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
15
frontend/public/manifest.json
Normal file
15
frontend/public/manifest.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"short_name": "Rezepte",
|
||||
"name": "TikTok Rezepte",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
"sizes": "64x64 32x32 24x24 16x16",
|
||||
"type": "image/x-icon"
|
||||
}
|
||||
],
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"theme_color": "#2563eb",
|
||||
"background_color": "#ffffff"
|
||||
}
|
||||
23
frontend/public/sw.js
Normal file
23
frontend/public/sw.js
Normal file
@@ -0,0 +1,23 @@
|
||||
const CACHE_NAME = 'rezepte-v1';
|
||||
const ASSETS = [
|
||||
'/',
|
||||
'/index.html',
|
||||
];
|
||||
|
||||
self.addEventListener('install', (event) => {
|
||||
event.waitUntil(caches.open(CACHE_NAME).then((cache) => cache.addAll(ASSETS)));
|
||||
});
|
||||
|
||||
self.addEventListener('fetch', (event) => {
|
||||
event.respondWith(
|
||||
caches.match(event.request).then((response) => response || fetch(event.request))
|
||||
);
|
||||
});
|
||||
|
||||
self.addEventListener('activate', (event) => {
|
||||
event.waitUntil(
|
||||
caches.keys().then((keys) =>
|
||||
Promise.all(keys.filter((key) => key !== CACHE_NAME).map((key) => caches.delete(key)))
|
||||
)
|
||||
);
|
||||
});
|
||||
@@ -21,12 +21,31 @@ function normalizeIngredients(ingredients) {
|
||||
);
|
||||
}
|
||||
|
||||
export default function IngredientList({ ingredients, editable, onChange }) {
|
||||
const [checked, setChecked] = useState({});
|
||||
export default function IngredientList({ ingredients, editable, onChange, multiplier = 1 }) {
|
||||
const [checked, setChecked] = useState(() => {
|
||||
try {
|
||||
const saved = localStorage.getItem(`ingredients-checked-${window.location.pathname}`);
|
||||
return saved ? JSON.parse(saved) : {};
|
||||
} catch { return {}; }
|
||||
});
|
||||
|
||||
const normalized = useMemo(() => normalizeIngredients(ingredients), [ingredients]);
|
||||
|
||||
const toggle = (i) => setChecked(prev => ({ ...prev, [i]: !prev[i] }));
|
||||
const toggle = (i) => {
|
||||
const newChecked = { ...checked, [i]: !checked[i] };
|
||||
setChecked(newChecked);
|
||||
localStorage.setItem(`ingredients-checked-${window.location.pathname}`, JSON.stringify(newChecked));
|
||||
};
|
||||
|
||||
const formatAmount = (amount, mult) => {
|
||||
if (!amount || mult === 1) return amount;
|
||||
// Versuche Zahl zu extrahieren und zu multiplizieren
|
||||
const num = parseFloat(amount.replace(',', '.'));
|
||||
if (isNaN(num)) return amount;
|
||||
const result = num * mult;
|
||||
// Formatiere zurück (z.B. 1.5 -> 1,5)
|
||||
return result.toLocaleString('de-DE', { maximumFractionDigits: 2 });
|
||||
};
|
||||
|
||||
const updateIngredient = (i, field, value) => {
|
||||
const updated = normalized.map((ing, idx) =>
|
||||
@@ -84,7 +103,7 @@ export default function IngredientList({ ingredients, editable, onChange }) {
|
||||
</>
|
||||
) : (
|
||||
<span className={`${checked[i] ? 'line-through text-gray-400' : ''}`}>
|
||||
{ing.amount && `${ing.amount} `}
|
||||
{ing.amount && `${formatAmount(ing.amount, multiplier)} `}
|
||||
{ing.unit && `${ing.unit} `}
|
||||
{ing.name}
|
||||
</span>
|
||||
|
||||
@@ -4,6 +4,12 @@ import { BrowserRouter } from 'react-router-dom';
|
||||
import App from './App';
|
||||
import './index.css';
|
||||
|
||||
if ('serviceWorker' in navigator) {
|
||||
window.addEventListener('load', () => {
|
||||
navigator.serviceWorker.register('/sw.js');
|
||||
});
|
||||
}
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')).render(
|
||||
<React.StrictMode>
|
||||
<BrowserRouter>
|
||||
|
||||
@@ -12,8 +12,9 @@ export default function RecipeDetail() {
|
||||
const { id } = useParams();
|
||||
const navigate = useNavigate();
|
||||
const { user } = useAuth();
|
||||
const [recipe, setRecipe] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [servings, setServings] = useState(2);
|
||||
const [baseServings, setBaseServings] = useState(2);
|
||||
|
||||
const [editing, setEditing] = useState(false);
|
||||
const [form, setForm] = useState({});
|
||||
|
||||
@@ -24,6 +25,10 @@ export default function RecipeDetail() {
|
||||
const data = await fetchRecipe(id);
|
||||
setRecipe(data);
|
||||
setForm(data);
|
||||
if (data.servings) {
|
||||
setServings(data.servings);
|
||||
setBaseServings(data.servings);
|
||||
}
|
||||
} catch (e) {
|
||||
navigate('/');
|
||||
}
|
||||
@@ -130,7 +135,28 @@ export default function RecipeDetail() {
|
||||
onChange={ingredients => updateField('ingredients', ingredients)}
|
||||
/>
|
||||
) : (
|
||||
<IngredientList ingredients={recipe.ingredients} />
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h3 className="font-semibold text-lg">Zutaten</h3>
|
||||
<div className="flex items-center gap-2 text-sm bg-gray-50 px-3 py-1 rounded-lg border border-gray-200">
|
||||
<span className="text-gray-500 font-medium">Portionen:</span>
|
||||
<button
|
||||
onClick={() => setServings(Math.max(1, servings - 1))}
|
||||
className="w-6 h-6 flex items-center justify-center bg-white border border-gray-300 rounded font-bold hover:bg-gray-100"
|
||||
>
|
||||
-
|
||||
</button>
|
||||
<span className="w-6 text-center font-semibold">{servings}</span>
|
||||
<button
|
||||
onClick={() => setServings(servings + 1)}
|
||||
className="w-6 h-6 flex items-center justify-center bg-white border border-gray-300 rounded font-bold hover:bg-gray-100"
|
||||
>
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<IngredientList ingredients={recipe.ingredients} multiplier={servings / baseServings} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{editing ? (
|
||||
|
||||
Reference in New Issue
Block a user