Initial commit - Meal Tracker app

This commit is contained in:
Otto
2026-03-30 16:03:05 -04:00
parent 139f756807
commit 02d8df5efb
3863 changed files with 773862 additions and 215 deletions

View File

@@ -1,25 +1,16 @@
import { BrowserRouter, Routes, Route } from 'react-router-dom'
import Daily from './pages/Daily'
import AddEntry from './pages/AddEntry'
import EntryDetail from './pages/EntryDetail'
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import Daily from './pages/Daily';
import AddEntry from './pages/AddEntry';
import EntryDetail from './pages/EntryDetail';
export default function App() {
return (
<BrowserRouter>
<div className="min-h-screen bg-gray-100">
<header className="bg-white shadow-sm sticky top-0 z-10">
<div className="max-w-md mx-auto px-4 py-3 flex items-center justify-between">
<h1 className="text-lg font-semibold text-gray-800">Meal Tracker</h1>
</div>
</header>
<main className="max-w-md mx-auto px-4 py-4">
<Routes>
<Route path="/" element={<Daily />} />
<Route path="/add" element={<AddEntry />} />
<Route path="/entry/:id" element={<EntryDetail />} />
</Routes>
</main>
</div>
<Routes>
<Route path="/" element={<Daily />} />
<Route path="/add" element={<AddEntry />} />
<Route path="/entry/:id" element={<EntryDetail />} />
</Routes>
</BrowserRouter>
)
}
);
}

View File

@@ -20,13 +20,12 @@ export default function FoodSearch({ onSelect, onClose }) {
}
const handleSelect = (item) => {
const product = item.product
onSelect({
name: product.product_name || product.product_name_en || query,
calories: Math.round(product.nutriments?.['energy-kcal_100g'] || 0),
protein: Math.round(product.nutriments?.proteins_100g || 0),
carbs: Math.round(product.nutriments?.carbohydrates_100g || 0),
fat: Math.round(product.nutriments?.fat_100g || 0)
name: item.name || item.product?.product_name || item.product?.product_name_en || query,
calories: Math.round(item.calories || item.product?.nutriments?.['energy-kcal_100g'] || 0),
protein: Math.round(item.protein || item.product?.nutriments?.proteins_100g || 0),
carbs: Math.round(item.carbs || item.product?.nutriments?.carbohydrates_100g || 0),
fat: Math.round(item.fat || item.product?.nutriments?.fat_100g || 0)
})
}

View File

@@ -0,0 +1,69 @@
import { useState, useEffect } from 'react';
import { Link, useSearchParams } from 'react-router-dom';
export default function Layout({ children }) {
const [darkMode, setDarkMode] = useState(true);
const [selectedDate, setSelectedDate] = useState(new Date().toISOString().split('T')[0]);
const [searchParams] = useSearchParams();
const returnDate = searchParams.get('date');
const isCurrentDay = selectedDate === new Date().toISOString().split('T')[0];
useEffect(() => {
setDarkMode(document.documentElement.classList.contains('dark'));
}, []);
useEffect(() => {
if (darkMode) {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
}, [darkMode]);
function handleDateChange(e) {
setSelectedDate(e.target.value);
}
function goToToday() {
setSelectedDate(new Date().toISOString().split('T')[0]);
}
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';
const textMuted = darkMode ? 'text-gray-400' : 'text-gray-500';
const inputBg = darkMode ? 'bg-gray-700 border-gray-600 text-white' : 'bg-white border-gray-300 text-gray-900';
return (
<div className={"min-h-screen " + bgMain}>
{/* Persistent Header */}
<div className={bgCard + " p-3 flex items-center justify-between sticky top-0 z-50 shadow-md"}>
<div className="flex items-center gap-2">
<Link to={'/?date=' + selectedDate} className="text-emerald-500 font-bold text-lg">🍽</Link>
<input
type="date"
value={selectedDate}
onChange={handleDateChange}
className={inputBg + " rounded px-2 py-1 text-sm"}
/>
{!isCurrentDay && (
<button onClick={goToToday} className="text-emerald-500 text-sm font-semibold">
Today
</button>
)}
</div>
<button
onClick={() => setDarkMode(!darkMode)}
className="w-10 h-10 rounded-full flex items-center justify-center text-xl"
>
{darkMode ? '☀️' : '🌙'}
</button>
</div>
<div className="p-4 max-w-md mx-auto">
{children}
</div>
</div>
);
}

View File

@@ -1,5 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; }

View File

@@ -1,91 +1,210 @@
import { useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { createEntry } from '../utils/api'
import FoodSearch from '../components/FoodSearch'
import { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import Layout from '../components/Layout';
export default function AddEntry() {
const navigate = useNavigate()
const [form, setForm] = useState({ name: '', description: '', notes: '', calories: '', protein: '', carbs: '', fat: '' })
const [image, setImage] = useState(null)
const [preview, setPreview] = useState(null)
const [showSearch, setShowSearch] = useState(false)
const [saving, setSaving] = useState(false)
const navigate = useNavigate();
const [darkMode, setDarkMode] = useState(true);
const [foods, setFoods] = useState([]);
const [searchQuery, setSearchQuery] = useState('');
const [showSearch, setShowSearch] = useState(false);
const [loading, setLoading] = useState(false);
const [form, setForm] = useState({
name: '',
description: '',
notes: '',
calories: '',
protein: '',
carbs: '',
fat: '',
image: null
});
const handleImage = (e) => {
const file = e.target.files[0]
if (file) {
setImage(file)
setPreview(URL.createObjectURL(file))
}
}
useEffect(() => {
setDarkMode(document.documentElement.classList.contains('dark'));
}, []);
const handleSearchSelect = (food) => {
setForm(f => ({ ...f, name: food.name, calories: food.calories, protein: food.protein, carbs: food.carbs, fat: food.fat }))
setShowSearch(false)
}
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';
const inputBg = darkMode ? 'bg-gray-700 border-gray-600 text-white' : 'bg-white border-gray-300 text-gray-900';
const handleSubmit = async (e) => {
e.preventDefault()
setSaving(true)
async function handleSearch() {
if (!searchQuery) return;
setLoading(true);
try {
const payload = new FormData()
payload.append('name', form.name)
payload.append('description', form.description)
payload.append('notes', form.notes)
payload.append('calories', form.calories || 0)
payload.append('protein', form.protein || 0)
payload.append('carbs', form.carbs || 0)
payload.append('fat', form.fat || 0)
if (image) payload.append('image', image)
await createEntry(payload)
navigate('/')
const res = await fetch('/api/foods/search?q=' + searchQuery);
const data = await res.json();
if (Array.isArray(data)) {
setFoods(data);
setShowSearch(true);
}
} catch (err) {
console.error('Failed to save', err)
alert('Failed to save entry')
console.error(err);
} finally {
setSaving(false)
setLoading(false);
}
}
function selectFood(food) {
setForm({
...form,
name: food.name,
calories: Math.round(food.calories),
protein: Math.round(food.protein),
carbs: Math.round(food.carbs),
fat: Math.round(food.fat)
});
setShowSearch(false);
}
function handleSubmit(e) {
e.preventDefault();
const formData = new FormData();
formData.append('name', form.name);
formData.append('description', form.description);
formData.append('notes', form.notes);
formData.append('calories', form.calories);
formData.append('protein', form.protein);
formData.append('carbs', form.carbs);
formData.append('fat', form.fat);
if (form.image) {
formData.append('image', form.image);
}
fetch('/api/entries', {
method: 'POST',
body: formData
})
.then(res => res.json())
.then(data => {
navigate('/');
})
.catch(err => {
console.error(err);
alert('Failed to save entry');
});
}
return (
<div>
<Layout>
<h1 className={"text-2xl font-bold mb-4 " + textMain}>Add Entry</h1>
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Food Name</label>
<input type="text" value={form.name} onChange={e => setForm({...form, name: e.target.value})} className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-emerald-500 focus:border-transparent" required />
{/* Food Search */}
<div className={bgCard + " rounded-xl shadow-md p-4"}>
<label className={"block text-sm font-medium mb-2 " + textMain}>Search Food (optional)</label>
<div className="flex gap-2">
<input
type="text"
value={searchQuery}
onChange={e => setSearchQuery(e.target.value)}
placeholder="Search food..."
className={"flex-1 px-3 py-2 rounded-lg border " + inputBg}
/>
<button type="button" onClick={handleSearch} className="bg-emerald-500 text-white px-4 py-2 rounded-lg">
Search
</button>
</div>
{showSearch && (
<div className="mt-3 max-h-40 overflow-y-auto">
{foods.length === 0 ? (
<p className={textMuted}>No results found</p>
) : (
foods.map((food, i) => (
<button
key={i}
type="button"
onClick={() => selectFood(food)}
className={"w-full text-left p-2 rounded hover:bg-gray-700 " + textMuted}
>
{food.name} {food.calories}cal P:{food.protein}g C:{food.carbs}g F:{food.fat}g
</button>
))
)}
</div>
)}
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Description</label>
<input type="text" value={form.description} onChange={e => setForm({...form, description: e.target.value})} className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-emerald-500 focus:border-transparent" />
{/* Name */}
<div className={bgCard + " rounded-xl shadow-md p-4"}>
<label className={"block text-sm font-medium mb-2 " + textMain}>Food Name *</label>
<input
type="text"
required
value={form.name}
onChange={e => setForm({...form, name: e.target.value})}
placeholder="e.g., Grilled Chicken"
className={"w-full px-3 py-2 rounded-lg border " + inputBg}
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Image</label>
<input type="file" accept="image/*" onChange={handleImage} className="w-full" />
{preview && <img src={preview} alt="Preview" className="mt-2 w-24 h-24 object-cover rounded-lg" />}
{/* Description */}
<div className={bgCard + " rounded-xl shadow-md p-4"}>
<label className={"block text-sm font-medium mb-2 " + textMain}>Description</label>
<input
type="text"
value={form.description}
onChange={e => setForm({...form, description: e.target.value})}
placeholder="e.g., Lunch"
className={"w-full px-3 py-2 rounded-lg border " + inputBg}
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Notes</label>
<textarea value={form.notes} onChange={e => setForm({...form, notes: e.target.value})} className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-emerald-500 focus:border-transparent" rows={2} />
{/* Macros */}
<div className={bgCard + " rounded-xl shadow-md p-4"}>
<label className={"block text-sm font-medium mb-2 " + textMain}>Macros</label>
<div className="grid grid-cols-4 gap-2">
<div>
<label className={"block text-xs " + textMuted}>Calories</label>
<input type="number" value={form.calories} onChange={e => setForm({...form, calories: e.target.value})} className={"w-full px-2 py-1 rounded border " + inputBg} />
</div>
<div>
<label className={"block text-xs " + textMuted}>Protein</label>
<input type="number" value={form.protein} onChange={e => setForm({...form, protein: e.target.value})} className={"w-full px-2 py-1 rounded border " + inputBg} />
</div>
<div>
<label className={"block text-xs " + textMuted}>Carbs</label>
<input type="number" value={form.carbs} onChange={e => setForm({...form, carbs: e.target.value})} className={"w-full px-2 py-1 rounded border " + inputBg} />
</div>
<div>
<label className={"block text-xs " + textMuted}>Fat</label>
<input type="number" value={form.fat} onChange={e => setForm({...form, fat: e.target.value})} className={"w-full px-2 py-1 rounded border " + inputBg} />
</div>
</div>
</div>
<div className="flex items-center gap-2">
<button type="button" onClick={() => setShowSearch(true)} className="px-3 py-2 bg-gray-200 rounded-lg text-sm hover:bg-gray-300">Search Food</button>
{/* Image */}
<div className={bgCard + " rounded-xl shadow-md p-4"}>
<label className={"block text-sm font-medium mb-2 " + textMain}>Photo</label>
<input
type="file"
accept="image/*"
onChange={e => setForm({...form, image: e.target.files[0]})}
className={"w-full " + textMuted}
/>
</div>
<div className="grid grid-cols-2 gap-3">
<div><label className="block text-xs text-gray-500 mb-1">Calories</label><input type="number" value={form.calories} onChange={e => setForm({...form, calories: e.target.value})} className="w-full px-3 py-2 border rounded-lg" /></div>
<div><label className="block text-xs text-gray-500 mb-1">Protein (g)</label><input type="number" value={form.protein} onChange={e => setForm({...form, protein: e.target.value})} className="w-full px-3 py-2 border rounded-lg" /></div>
<div><label className="block text-xs text-gray-500 mb-1">Carbs (g)</label><input type="number" value={form.carbs} onChange={e => setForm({...form, carbs: e.target.value})} className="w-full px-3 py-2 border rounded-lg" /></div>
<div><label className="block text-xs text-gray-500 mb-1">Fat (g)</label><input type="number" value={form.fat} onChange={e => setForm({...form, fat: e.target.value})} className="w-full px-3 py-2 border rounded-lg" /></div>
{/* Notes */}
<div className={bgCard + " rounded-xl shadow-md p-4"}>
<label className={"block text-sm font-medium mb-2 " + textMain}>Notes</label>
<textarea
value={form.notes}
onChange={e => setForm({...form, notes: e.target.value})}
placeholder="Any notes..."
rows={3}
className={"w-full px-3 py-2 rounded-lg border " + inputBg}
/>
</div>
<button type="submit" disabled={saving} className="w-full py-3 bg-emerald-600 text-white rounded-lg font-medium hover:bg-emerald-700 disabled:opacity-50">{saving ? 'Saving...' : 'Save Entry'}</button>
{/* Submit */}
<button type="submit" className="w-full bg-emerald-500 hover:bg-emerald-600 text-white py-3 rounded-xl font-semibold">
Save Entry
</button>
</form>
{showSearch && <FoodSearch onSelect={handleSearchSelect} onClose={() => setShowSearch(false)} />}
</div>
)
}
</Layout>
);
}

View File

@@ -1,78 +1,141 @@
import { useState, useEffect } from 'react'
import { Link } from 'react-router-dom'
import { getEntries } from '../utils/api'
import { useState, useEffect } from 'react';
import { Link } from 'react-router-dom';
import Layout from '../components/Layout';
export default function Daily() {
const [entries, setEntries] = useState([])
const [loading, setLoading] = useState(true)
const today = new Date().toISOString().split('T')[0]
const [entries, setEntries] = useState([]);
const [summary, setSummary] = useState({ total_calories: 0, total_protein: 0, total_carbs: 0, total_fat: 0 });
const [loading, setLoading] = useState(true);
const [selectedDate, setSelectedDate] = useState(new Date().toISOString().split('T')[0]);
const [isCurrentDay, setIsCurrentDay] = useState(true);
const [darkMode, setDarkMode] = useState(true);
useEffect(() => {
loadEntries()
}, [])
loadData();
}, [selectedDate]);
const loadEntries = async () => {
useEffect(() => {
const today = new Date().toISOString().split('T')[0];
setIsCurrentDay(selectedDate === today);
setDarkMode(document.documentElement.classList.contains('dark'));
}, [selectedDate]);
async function loadData() {
try {
const data = await getEntries()
const todayEntries = data.filter(e => e.date?.startsWith(today) || e.createdAt?.startsWith(today))
setEntries(todayEntries)
setLoading(true);
const entriesRes = await fetch('/api/entries?date=' + selectedDate);
const entriesData = await entriesRes.json();
const summaryRes = await fetch('/api/summary/daily?date=' + selectedDate);
const summaryData = await summaryRes.json();
setEntries(entriesData);
setSummary(summaryData);
} catch (err) {
console.error('Failed to load entries', err)
console.error('Failed to load:', err);
} finally {
setLoading(false)
setLoading(false);
}
}
const totalMacros = entries.reduce((acc, e) => ({
calories: acc.calories + (e.calories || 0),
protein: acc.protein + (e.protein || 0),
carbs: acc.carbs + (e.carros || 0),
fat: acc.fat + (e.fat || 0)
}), { calories: 0, protein: 0, carbs: 0, fat: 0 })
function formatDate(dateStr) {
const date = new Date(dateStr);
return date.toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric' });
}
if (loading) return <div className="text-center py-8 text-gray-500">Loading...</div>
function handleDateChange(e) {
setSelectedDate(e.target.value);
}
function goToToday() {
setSelectedDate(new Date().toISOString().split('T')[0]);
}
function toggleTheme() {
setDarkMode(!darkMode);
if (darkMode) {
document.documentElement.classList.remove('dark');
} else {
document.documentElement.classList.add('dark');
}
}
const bgMain = darkMode ? 'bg-gray-900' : 'bg-gray-100';
const bgCard = darkMode ? 'bg-gray-800' : 'bg-white';
const bgSummary = darkMode ? 'bg-emerald-700' : 'bg-emerald-600';
const textMain = darkMode ? 'text-white' : 'text-gray-900';
const textMuted = darkMode ? 'text-gray-400' : 'text-gray-500';
const inputBg = darkMode ? 'bg-gray-700 border-gray-600 text-white' : 'bg-white border-gray-300 text-gray-900';
if (loading) return <Layout><div className="p-4">Loading...</div></Layout>;
return (
<div>
<div className="bg-white rounded-lg p-4 mb-4 shadow-sm">
<h2 className="text-sm font-medium text-gray-500 mb-2">Today's Totals</h2>
<div className="flex justify-between text-center">
<div><div className="text-lg font-semibold">{totalMacros.calories}</div><div className="text-xs text-gray-500">cal</div></div>
<div><div className="text-lg font-semibold">{totalMacros.protein}g</div><div className="text-xs text-gray-500">protein</div></div>
<div><div className="text-lg font-semibold">{totalMacros.carbs}g</div><div className="text-xs text-gray-500">carbs</div></div>
<div><div className="text-lg font-semibold">{totalMacros.fat}g</div><div className="text-xs text-gray-500">fat</div></div>
<Layout>
<h1 className={"text-2xl font-bold mb-2 " + textMain}>
{isCurrentDay ? "Today's Meals" : formatDate(selectedDate)}
</h1>
{/* Summary Card */}
<div className={bgSummary + " text-white p-4 rounded-xl mb-4 shadow-lg"}>
<div className="grid grid-cols-4 gap-2 text-center">
<div>
<div className="text-xs opacity-80">Cal</div>
<div className="font-bold text-lg">{summary.total_calories || 0}</div>
</div>
<div>
<div className="text-xs opacity-80">Protein</div>
<div className="font-bold text-lg">{summary.total_protein || 0}g</div>
</div>
<div>
<div className="text-xs opacity-80">Carbs</div>
<div className="font-bold text-lg">{summary.total_carbs || 0}g</div>
</div>
<div>
<div className="text-xs opacity-80">Fat</div>
<div className="font-bold text-lg">{summary.total_fat || 0}g</div>
</div>
</div>
</div>
{entries.length === 0 ? (
<div className="text-center py-8 text-gray-500">No meals logged today</div>
) : (
<div className="space-y-3">
{entries.map(entry => (
<Link key={entry._id || entry.id} to={`/entry/${entry._id || entry.id}`} className="block bg-white rounded-lg p-4 shadow-sm hover:shadow-md transition-shadow">
{/* Entries List */}
<div className="space-y-3 mb-20">
{entries.length === 0 ? (
<div className={bgCard + " rounded-xl p-8 text-center " + textMuted}>
<div className="text-4xl mb-2">🍽</div>
<p>No meals logged for this day</p>
</div>
) : (
entries.map(entry => (
<Link
key={entry.id}
to={'/entry/' + entry.id + '?date=' + selectedDate}
className={bgCard + " rounded-xl shadow-md p-3 block hover:shadow-lg transition-shadow"}
>
<div className="flex gap-3">
{entry.image && (
<img src={entry.image} alt="" className="w-16 h-16 object-cover rounded-lg flex-shrink-0" />
{entry.image_url && (
<img
src={'http://10.10.10.143:3000' + entry.image_url}
alt=""
className="w-16 h-16 object-cover rounded-lg flex-shrink-0"
/>
)}
<div className="flex-1 min-w-0">
<h3 className="font-medium text-gray-800 truncate">{entry.name}</h3>
<p className="text-sm text-gray-500 truncate">{entry.description}</p>
<div className="mt-1 flex gap-2 text-xs text-gray-400">
<span>{entry.calories || 0} cal</span>
<span>P: {entry.protein || 0}g</span>
<span>C: {entry.carbs || 0}g</span>
<span>F: {entry.fat || 0}g</span>
<div className="flex-1">
<div className={"font-semibold " + textMain}>{entry.name}</div>
{entry.description && <div className={"text-sm " + textMuted}>{entry.description}</div>}
<div className={"text-xs mt-1 " + textMuted}>
{entry.calories || 0} cal P: {entry.protein || 0}g C: {entry.carbs || 0}g F: {entry.fat || 0}g
</div>
{entry.notes && <p className="text-xs text-gray-400 mt-1 truncate">{entry.notes}</p>}
</div>
</div>
</Link>
))}
</div>
)}
))
)}
</div>
<Link to="/add" className="fixed bottom-6 right-6 bg-emerald-600 text-white w-14 h-14 rounded-full flex items-center justify-center shadow-lg hover:bg-emerald-700 transition-colors text-2xl">+</Link>
</div>
)
}
{/* Add Button */}
{isCurrentDay && (
<Link to="/add" className="fixed bottom-6 right-6 bg-emerald-500 hover:bg-emerald-600 text-white w-14 h-14 rounded-full flex items-center justify-center text-2xl shadow-xl">
+
</Link>
)}
</Layout>
);
}

View File

@@ -1,73 +1,97 @@
import { useState, useEffect } from 'react'
import { useParams, useNavigate, Link } from 'react-router-dom'
import { getEntries, deleteEntry } from '../utils/api'
import { useState, useEffect } from 'react';
import { useParams, useSearchParams, Link } from 'react-router-dom';
import Layout from '../components/Layout';
export default function EntryDetail() {
const { id } = useParams()
const navigate = useNavigate()
const [entry, setEntry] = useState(null)
const [loading, setLoading] = useState(true)
const [deleting, setDeleting] = useState(false)
const { id } = useParams();
const [searchParams] = useSearchParams();
const returnDate = searchParams.get('date') || '';
const [entry, setEntry] = useState(null);
const [loading, setLoading] = useState(true);
const [darkMode, setDarkMode] = useState(true);
useEffect(() => {
loadEntry()
}, [id])
setDarkMode(document.documentElement.classList.contains('dark'));
}, []);
const loadEntry = async () => {
try {
const entries = await getEntries()
const found = entries.find(e => (e._id || e.id) === id)
setEntry(found)
} catch (err) {
console.error('Failed to load entry', err)
} finally {
setLoading(false)
useEffect(() => {
async function load() {
try {
const res = await fetch('/api/entries');
const entries = await res.json();
const found = entries.find(e => String(e.id) === String(id));
setEntry(found);
} catch (err) {
console.error(err);
} finally {
setLoading(false);
}
}
load();
}, [id]);
async function handleDelete() {
if (!confirm('Delete this entry?')) return;
await fetch('/api/entries/' + id, { method: 'DELETE' });
window.location.href = returnDate ? '/?date=' + returnDate : '/';
}
const handleDelete = async () => {
if (!confirm('Delete this entry?')) return
setDeleting(true)
try {
await deleteEntry(id)
navigate('/')
} catch (err) {
console.error('Failed to delete', err)
alert('Failed to delete')
setDeleting(false)
}
}
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';
if (loading) return <div className="text-center py-8 text-gray-500">Loading...</div>
if (!entry) return <div className="text-center py-8 text-gray-500">Entry not found</div>
if (loading) return <Layout><div className="p-4">Loading...</div></Layout>;
if (!entry) return <Layout><div className={"p-4 " + textMain}>Entry not found</div></Layout>;
const returnUrl = returnDate ? '/?date=' + returnDate : '/';
return (
<div>
{entry.image && <img src={entry.image} alt={entry.name} className="w-full h-64 object-cover rounded-lg mb-4" />}
<Layout>
<Link to={returnUrl} className="text-emerald-500 mb-4 block"> Back</Link>
<div className="bg-white rounded-lg p-4 shadow-sm mb-4">
<h1 className="text-xl font-semibold text-gray-800 mb-2">{entry.name}</h1>
{entry.description && <p className="text-gray-600 mb-4">{entry.description}</p>}
<div className="grid grid-cols-4 gap-2 text-center py-3 border-t">
<div><div className="text-lg font-semibold">{entry.calories || 0}</div><div className="text-xs text-gray-500">cal</div></div>
<div><div className="text-lg font-semibold">{entry.protein || 0}g</div><div className="text-xs text-gray-500">protein</div></div>
<div><div className="text-lg font-semibold">{entry.carbs || 0}g</div><div className="text-xs text-gray-500">carbs</div></div>
<div><div className="text-lg font-semibold">{entry.fat || 0}g</div><div className="text-xs text-gray-500">fat</div></div>
<h1 className={"text-2xl font-bold mb-2 " + textMain}>{entry.name}</h1>
{entry.description && <p className={textMuted + " mb-4"}>{entry.description}</p>}
{entry.image_url && (
<img
src={'http://10.10.10.143:3000' + entry.image_url}
alt=""
className="w-full rounded-xl mb-4"
/>
)}
<div className={bgCard + " rounded-xl shadow-md p-4 mb-4"}>
<h2 className={"font-semibold mb-2 " + textMain}>Macros</h2>
<div className="grid grid-cols-4 gap-2 text-center">
<div>
<div className={"text-xs " + textMuted}>Cal</div>
<div className={"font-bold text-lg " + textMain}>{entry.calories || 0}</div>
</div>
<div>
<div className={"text-xs " + textMuted}>Protein</div>
<div className={"font-bold text-lg " + textMain}>{entry.protein || 0}g</div>
</div>
<div>
<div className={"text-xs " + textMuted}>Carbs</div>
<div className={"font-bold text-lg " + textMain}>{entry.carbs || 0}g</div>
</div>
<div>
<div className={"text-xs " + textMuted}>Fat</div>
<div className={"font-bold text-lg " + textMain}>{entry.fat || 0}g</div>
</div>
</div>
</div>
{entry.notes && (
<div className="bg-white rounded-lg p-4 shadow-sm mb-4">
<h2 className="text-sm font-medium text-gray-500 mb-2">Notes</h2>
<p className="text-gray-700">{entry.notes}</p>
<div className={bgCard + " rounded-xl shadow-md p-4 mb-4"}>
<h2 className={"font-semibold mb-1 " + textMain}>Notes</h2>
<p className={textMuted}>{entry.notes}</p>
</div>
)}
<div className="flex gap-3">
<Link to="/" className="flex-1 py-3 bg-gray-200 text-center rounded-lg font-medium hover:bg-gray-300">Back</Link>
<button onClick={handleDelete} disabled={deleting} className="flex-1 py-3 bg-red-600 text-white rounded-lg font-medium hover:bg-red-700 disabled:opacity-50">{deleting ? 'Deleting...' : 'Delete'}</button>
</div>
</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>
</Layout>
);
}

View File

@@ -1,7 +1,7 @@
import axios from 'axios'
const api = axios.create({
baseURL: '/api',
baseURL: 'http://10.10.10.143:3000/api',
headers: { 'Content-Type': 'application/json' }
})