diff --git a/scratch/inspect-meez-page-layout.cjs b/scratch/inspect-meez-page-layout.cjs new file mode 100644 index 0000000..22d05d3 --- /dev/null +++ b/scratch/inspect-meez-page-layout.cjs @@ -0,0 +1,98 @@ +const fs = require('fs'); + +async function main() { + const pages = await fetch('http://localhost:9222/json').then(r => r.json()); + const meezPage = pages.find(p => p.url.includes('getmeez.com/recipes/')); + if (!meezPage) return console.log('No recipe page found'); + + const ws = new WebSocket(meezPage.webSocketDebuggerUrl); + let id = 1; + const send = (method, params = {}) => new Promise((resolve, reject) => { + const msgId = id++; + const handler = (event) => { + const res = JSON.parse(event.data); + if (res.id === msgId) { + ws.removeEventListener('message', handler); + if (res.error) reject(res.error); + else resolve(res.result); + } + }; + ws.addEventListener('message', handler); + ws.send(JSON.stringify({ id: msgId, method, params })); + }); + + ws.addEventListener('open', async () => { + try { + const evalRes = await send('Runtime.evaluate', { + expression: `(() => { + const metaItem = document.querySelector('[data-testid="metaItem"]'); + if (!metaItem) return { error: 'metaItem not found' }; + + const rect = metaItem.getBoundingClientRect(); + const parent = metaItem.parentElement; + const grandParent = parent ? parent.parentElement : null; + + // Find recipe title + const title = document.querySelector('h1, [data-testid*="title"], [data-testid*="Title"]'); + const titleRect = title ? title.getBoundingClientRect() : null; + + // Find ingredients table / container + const ingContainer = document.querySelector('[data-testid*="Ingredients"], [data-testid*="RecipeTable"], table, [role="table"]'); + const ingRect = ingContainer ? ingContainer.getBoundingClientRect() : null; + + // Collect siblings of metaItem + const siblings = parent ? Array.from(parent.children).map(c => ({ + tag: c.tagName, + class: c.className, + testId: c.getAttribute('data-testid'), + rect: c.getBoundingClientRect(), + text: c.innerText?.slice(0, 100) + })) : []; + + // Collect ancestors up to body + const ancestors = []; + let cur = metaItem; + while (cur && cur !== document.body) { + ancestors.push({ + tag: cur.tagName, + class: cur.className, + testId: cur.getAttribute('data-testid'), + rect: cur.getBoundingClientRect(), + style: { + display: window.getComputedStyle(cur).display, + position: window.getComputedStyle(cur).position, + padding: window.getComputedStyle(cur).padding, + margin: window.getComputedStyle(cur).margin, + gap: window.getComputedStyle(cur).gap + } + }); + cur = cur.parentElement; + } + + return { + metaItemRect: rect, + titleRect, + ingRect, + titleHtml: title?.outerHTML, + parentHtml: parent?.outerHTML?.slice(0, 500), + siblings, + ancestors + }; + })()`, + returnByValue: true + }); + + fs.writeFileSync('scratch/meez_page_layout.json', JSON.stringify(evalRes.result.value, null, 2)); + console.log('Saved to scratch/meez_page_layout.json'); + console.log('Ancestors count:', evalRes.result.value.ancestors?.length); + console.log('Siblings:\n', evalRes.result.value.siblings?.map(s => `${s.tag} (${s.testId || s.class}): ${s.text?.slice(0, 40)}`)); + + ws.close(); + } catch (e) { + console.error(e); + ws.close(); + } + }); +} + +main().catch(console.error); diff --git a/scratch/inspect-meez-structure.cjs b/scratch/inspect-meez-structure.cjs new file mode 100644 index 0000000..7a3898e --- /dev/null +++ b/scratch/inspect-meez-structure.cjs @@ -0,0 +1,74 @@ +const fs = require('fs'); + +async function main() { + const pages = await fetch('http://localhost:9222/json').then(r => r.json()); + const meezPage = pages.find(p => p.url.includes('getmeez.com/recipes/')); + if (!meezPage) return console.log('No recipe page found'); + + const ws = new WebSocket(meezPage.webSocketDebuggerUrl); + let id = 1; + const send = (method, params = {}) => new Promise((resolve, reject) => { + const msgId = id++; + const handler = (event) => { + const res = JSON.parse(event.data); + if (res.id === msgId) { + ws.removeEventListener('message', handler); + if (res.error) reject(res.error); + else resolve(res.result); + } + }; + ws.addEventListener('message', handler); + ws.send(JSON.stringify({ id: msgId, method, params })); + }); + + ws.addEventListener('open', async () => { + try { + const evalRes = await send('Runtime.evaluate', { + expression: `(() => { + // Get the entire recipe layout tree from the main container + const main = document.querySelector('[data-testid="RoutesBase"]') || document.querySelector('main') || document.querySelector('#root'); + + const header = document.querySelector('[data-testid*="Header"], header'); + const title = document.querySelector('h1, [data-testid*="EditableEntityNameBase_name"]'); + const titleParent = title ? title.closest('[data-testid*="Header"], div') : null; + + return { + titleParentHtml: titleParent ? { + tag: titleParent.tagName, + class: titleParent.className, + testId: titleParent.getAttribute('data-testid'), + html: titleParent.outerHTML, + style: { + padding: window.getComputedStyle(titleParent).padding, + margin: window.getComputedStyle(titleParent).margin + } + } : null, + leftColumnChildren: (() => { + const leftCol = document.querySelector('[data-testid="RoutesBase_section"]'); + if (!leftCol) return null; + return Array.from(leftCol.children).map(c => ({ + tag: c.tagName, + class: c.className, + testId: c.getAttribute('data-testid'), + text: c.innerText?.slice(0, 80), + html: c.outerHTML.slice(0, 200) + })); + })() + }; + })()`, + returnByValue: true + }); + + fs.writeFileSync('scratch/meez_structure.json', JSON.stringify(evalRes.result.value, null, 2)); + console.log('Structure saved.'); + console.log(JSON.stringify(evalRes.result.value, null, 2)); + + ws.close(); + } catch (e) { + console.error(e); + ws.close(); + } + }); +} + +main().catch(console.error); diff --git a/scratch/meez_page_layout.json b/scratch/meez_page_layout.json new file mode 100644 index 0000000..55e50e4 --- /dev/null +++ b/scratch/meez_page_layout.json @@ -0,0 +1,148 @@ +{ + "metaItemRect": {}, + "titleRect": {}, + "ingRect": {}, + "titleHtml": "

Marinated Pork Belly

", + "parentHtml": "
Batch Size:
", + "style": { + "padding": "0px", + "margin": "0px" + } + }, + "leftColumnChildren": [ + { + "tag": "DIV", + "class": "jss414", + "testId": "ItemsBase", + "text": "Batch Size:\n1x\nYield:\n\n2000\n\ng\n\nCut & Portion the Pork\n\n\n\n1800\n\ng\n\n\tSous Vide Po", + "html": "
`
- +
)} diff --git a/src/components/RecipeCalculator.tsx b/src/components/RecipeCalculator.tsx index ebda9c9..92c7926 100644 --- a/src/components/RecipeCalculator.tsx +++ b/src/components/RecipeCalculator.tsx @@ -19,6 +19,7 @@ type Props = { showDerived?: boolean; showPercentControls?: boolean; yieldConversions?: CalculatorItem["measureConversions"]; + shelfLife?: string | { duration?: { quantity?: number; unit_id?: string }; storage_condition?: string }; }; function itemUnits(item: CalculatorItem, units: Record) { @@ -47,13 +48,26 @@ function convertItem(quantity: number, fromUnitId: string, toUnitId: string, ite throw new Error(`No reviewed equivalency from ${fromUnitId} to ${toUnitId}`); } -export default function RecipeCalculator({ components, units, basisUnitId, yieldQuantity, yieldUnitId, nutrition, cost, servings, showDerived = true, showPercentControls = true, yieldConversions = [] }: Props) { +export default function RecipeCalculator({ components, units, basisUnitId, yieldQuantity, yieldUnitId, nutrition, cost, servings, showDerived = true, showPercentControls = true, yieldConversions = [], shelfLife }: Props) { const [factor, setFactor] = useState(1); const [calculatePercent,setCalculatePercent]=useState(showPercentControls); const [percentMode,setPercentMode]=useState<"standard"|"bakers">("standard"); const [yieldDisplayUnitId, setYieldDisplayUnitId] = useState(yieldUnitId); const [lineUnits, setLineUnits] = useState>({}); const validFactor = Number.isFinite(factor) && factor >= 0 ? factor : 0; + + const shelfLifeText = useMemo(() => { + if (!shelfLife) return null; + if (typeof shelfLife === "string") return shelfLife.trim() || null; + const q = shelfLife.duration?.quantity; + const u = shelfLife.duration?.unit_id; + if (q != null && u) { + const unitLabel = q === 1 ? u : `${u}s`; + return `${q} ${unitLabel}`; + } + return null; + }, [shelfLife]); + useEffect(() => { window.dispatchEvent(new CustomEvent("recipe-scale-change", { detail:validFactor })); }, [validFactor]); const itemWeight=(item:CalculatorItem)=>{try{return convertItem(item.amount.quantity,item.amount.unit_id,"gram",item,units);}catch{return undefined;}}; const total = useMemo( @@ -77,8 +91,8 @@ export default function RecipeCalculator({ components, units, basisUnitId, yield return (
-
-