import { useEffect, useMemo, useRef, useState } from 'react';
import { useAuth, useCollection, renderChart } from '@deplixo/sdk';
import { RecipeList } from './components/RecipeList.jsx';
import { RecipeEditor } from './components/RecipeEditor.jsx';
import { CostComparison } from './components/CostComparison.jsx';
function MiniBarChart({ data, label, emptyMessage = 'No data available.' }) {
const canvasRef = useRef(null);
useEffect(() => {
if (!canvasRef.current || !Array.isArray(data) || data.length === 0) return;
renderChart(canvasRef.current, {
type: 'bar',
data: {
labels: data.map(d => d.label),
datasets: [
{
label,
data: data.map(d => d.value),
},
],
},
});
}, [data, label]);
if (!Array.isArray(data) || data.length === 0) {
return
;
}
return ;
}
function App() {
const { user, loading, logout } = useAuth();
const [activeTab, setActiveTab] = useState('recipes');
const [editingRecipe, setEditingRecipe] = useState(null);
const [ingredientSearch, setIngredientSearch] = useState('');
const [priceDraft, setPriceDraft] = useState({ name: '', unit: 'unit', price: '', notes: '' });
const [refreshTick, setRefreshTick] = useState(0);
const pricesCollectionName = useMemo(() => {
const key = user?.id || user?.email || 'unknown';
return `ingredient_prices_${key}`;
}, [user?.id, user?.email]);
const {
items: ingredientPrices,
loading: pricesLoading,
add: addPrice,
update: updatePrice,
remove: removePrice,
search: searchPrices,
} = useCollection(pricesCollectionName, { personal: true });
const tabs = [
{ id: 'recipes', label: '๐ My Recipes', icon: '๐' },
{ id: 'new', label: 'โ New Recipe', icon: 'โ' },
{ id: 'compare', label: 'โ Compare', icon: 'โ' },
{ id: 'visualize', label: '๐ Visualize', icon: '๐' },
{ id: 'prices', label: '๐งพ Ingredient Prices', icon: '๐งพ' },
];
useEffect(() => {
if (user && activeTab === 'recipes' && !editingRecipe) {
const timer = setTimeout(() => setRefreshTick(t => t + 1), 5 * 60 * 1000);
return () => clearTimeout(timer);
}
}, [user, activeTab, editingRecipe, refreshTick]);
useEffect(() => {
if (!user) return;
const interval = setInterval(() => setRefreshTick(t => t + 1), 10 * 60 * 1000);
return () => clearInterval(interval);
}, [user]);
const normalizedIngredientPrices = useMemo(() => {
const items = Array.isArray(ingredientPrices) ? ingredientPrices : [];
return [...items].sort((a, b) => {
const aTime = new Date(a.updatedAt || a.createdAt || 0).getTime();
const bTime = new Date(b.updatedAt || b.createdAt || 0).getTime();
return bTime - aTime;
});
}, [ingredientPrices, refreshTick]);
const filteredIngredientPrices = useMemo(() => {
const q = ingredientSearch.trim().toLowerCase();
if (!q) return normalizedIngredientPrices;
return normalizedIngredientPrices.filter(item => {
const name = String(item.name || '').toLowerCase();
const unit = String(item.unit || '').toLowerCase();
const notes = String(item.notes || '').toLowerCase();
return name.includes(q) || unit.includes(q) || notes.includes(q);
});
}, [normalizedIngredientPrices, ingredientSearch]);
const recipeCostChartData = useMemo(() => {
const recipes = Array.isArray(searchPrices?.recipes) ? searchPrices.recipes : [];
return recipes
.map(recipe => ({
label: recipe.name || 'Untitled',
value: Number(recipe.totalCost ?? recipe.cost ?? 0),
}))
.sort((a, b) => b.value - a.value)
.slice(0, 12);
}, [searchPrices]);
const ingredientTrendData = useMemo(() => {
const grouped = new Map();
for (const item of normalizedIngredientPrices) {
const key = `${String(item.name || '').trim().toLowerCase()}__${String(item.unit || '').trim().toLowerCase()}`;
if (!grouped.has(key)) grouped.set(key, []);
grouped.get(key).push(item);
}
return [...grouped.entries()].map(([key, items]) => {
const sorted = [...items].sort((a, b) => {
const aTime = new Date(a.updatedAt || a.createdAt || 0).getTime();
const bTime = new Date(b.updatedAt || b.createdAt || 0).getTime();
return aTime - bTime;
});
const latest = sorted[sorted.length - 1] || {};
return {
key,
name: latest.name || 'Ingredient',
unit: latest.unit || 'unit',
data: sorted.map((item, index) => ({
label: new Date(item.updatedAt || item.createdAt || Date.now()).toLocaleDateString(),
value: Number(item.price || 0),
id: item.id || `${key}_${index}`,
})),
};
}).slice(0, 6);
}, [normalizedIngredientPrices]);
const handleEdit = (recipe) => {
setEditingRecipe(recipe);
setActiveTab('new');
};
const handleSaved = () => {
setEditingRecipe(null);
setActiveTab('recipes');
};
const handleNewTab = () => {
if (activeTab === 'new' && !editingRecipe) return;
setEditingRecipe(null);
setActiveTab('new');
};
const handlePriceSubmit = async (e) => {
e.preventDefault();
const name = priceDraft.name.trim();
const unit = priceDraft.unit.trim() || 'unit';
const price = Number(priceDraft.price);
if (!name || !Number.isFinite(price)) return;
const existing = normalizedIngredientPrices.find(item => String(item.name || '').toLowerCase() === name.toLowerCase() && String(item.unit || '').toLowerCase() === unit.toLowerCase());
const payload = {
name,
unit,
price,
notes: priceDraft.notes.trim(),
updatedAt: new Date().toISOString(),
refreshedAt: new Date().toISOString(),
};
if (existing) {
await updatePrice(existing.id, payload);
} else {
await addPrice({ ...payload, createdAt: new Date().toISOString() });
}
setPriceDraft({ name: '', unit: 'unit', price: '', notes: '' });
};
const handleRefreshPrices = async () => {
const now = new Date().toISOString();
for (const item of normalizedIngredientPrices) {
await updatePrice(item.id, { ...item, refreshedAt: now, updatedAt: now });
}
setRefreshTick(t => t + 1);
};
if (loading) {
return Signing in...
;
}
if (!user) {
return (
๐
Protected recipe workspace
Your saved recipes and ingredient price database are private to your account.
Sign in to create, compare, and manage your costs.
Deplixo will handle Google sign-in for you.
);
}
return (
{activeTab === 'recipes' && }
{activeTab === 'new' && }
{activeTab === 'compare' && }
{activeTab === 'visualize' && (
Recipe cost comparison
Bar chart of recipe total costs for quick side-by-side comparison.
Ingredient price trends
Trend charts use your stored ingredient price history over time.
{ingredientTrendData.length === 0 ? (
๐
No ingredient history yet
Add and update ingredient prices to see trend charts over time.
) : ingredientTrendData.map(series => (
{series.name}
{series.unit}
))}
)}
{activeTab === 'prices' && (
Personal ingredient price database
Stored privately per user. Add prices once and reuse them across recipes.
setIngredientSearch(e.target.value)}
placeholder="Search ingredient prices"
/>
{filteredIngredientPrices.length === 0 ? (
๐งพ
No ingredient prices yet
Add your first personal price to reuse it in recipes.
) : filteredIngredientPrices.map(item => (
{item.name}
{item.unit}
Price
${Number(item.price || 0).toFixed(2)}
Updated
{item.updatedAt ? new Date(item.updatedAt).toLocaleDateString() : 'โ'}
{item.notes || 'No notes saved.'}
))}
)}
);
}
ReactDOM.createRoot(document.getElementById('root')).render();