From e4e8f205555c1dc0eee267e829332412612d79fb Mon Sep 17 00:00:00 2001 From: Kavi Date: Tue, 2 Jun 2026 14:44:20 -0400 Subject: [PATCH] Twemoji category icons as logo fallback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit For transactions with no brand match, show the spending category emoji (Twitter Twemoji SVG via jsDelivr) instead of a bare initial: Groceries ๐Ÿ›’ Food ๐Ÿ” Fuel โ›ฝ Health ๐Ÿ’Š Subscriptions ๐Ÿ’ป Shopping ๐Ÿ› Utilities ๐Ÿ’ก Cash ๐Ÿง Debt ๐Ÿ’ณ Wallets ๐Ÿ“ฑ Fees ๐Ÿฆ Income/transfer rows use flow-type icons (๐Ÿ’ฐ income, ๐Ÿ” self-transfer). Resolution chain: Brandfetch brand logo -> category Twemoji -> coloured initial. Brand logo onerror also falls back to the category emoji. Call sites pass {category} (merchants/breakdown) or {flowType} (ledger). --- web/dashboard.css | 2 ++ web/dashboard.js | 4 ++-- web/dashboard2.js | 2 +- web/logos.js | 46 ++++++++++++++++++++++++++++++++++++++-------- 4 files changed, 43 insertions(+), 11 deletions(-) diff --git a/web/dashboard.css b/web/dashboard.css index 9984ae5..089d584 100644 --- a/web/dashboard.css +++ b/web/dashboard.css @@ -220,3 +220,5 @@ .mrow .m-name { display: flex; align-items: center; gap: 9px; } .bk-row-desc.with-logo { display: flex; align-items: center; gap: 8px; } .lx-desc .lx-logo-wrap { display: inline-flex; align-items: center; gap: 8px; } +.mlogo-cat { display: inline-flex; align-items: center; justify-content: center; border-radius: 6px; background: rgba(255,255,255,0.06); vertical-align: middle; flex: none; } +.mlogo-cat img { display: block; } diff --git a/web/dashboard.js b/web/dashboard.js index 587442d..fab5476 100644 --- a/web/dashboard.js +++ b/web/dashboard.js @@ -158,7 +158,7 @@ merch.forEach(([name, d], i) => { const cat = window.MT.spendCategory(d.txns[0].description); const row = document.createElement('div'); row.className = 'mrow'; - const logo = window.MTlogo ? window.MTlogo.html(name, 22) : ''; + const logo = window.MTlogo ? window.MTlogo.html(name, 22, {category: cat}) : ''; row.innerHTML = `${i + 1}${logo}${esc(name)}${esc(cat)}${d.txns.length}ร—${window.MT.CLPk(d.value)}`; row.addEventListener('mousemove', e => window.MTtip.rows(e, name, col.spend(), d.txns)); row.addEventListener('mouseleave', () => window.MTtip.hide()); @@ -180,7 +180,7 @@ section.className = 'bk-section'; const rowsHtml = descs.slice(0, 30).map(([desc, d]) => { const cpct = Math.round(d.value / catData.value * 100); - const blogo = window.MTlogo ? window.MTlogo.html(desc, 18) : ""; + const blogo = window.MTlogo ? window.MTlogo.html(desc, 18, {category: catName}) : ""; return `
${d.txns.length}ร— diff --git a/web/dashboard2.js b/web/dashboard2.js index ad44dae..7ebdb14 100644 --- a/web/dashboard2.js +++ b/web/dashboard2.js @@ -285,7 +285,7 @@ const shown = rows.slice(0, st.limit); $('#lx-rows').innerHTML = shown.map(t => ` ${t.date} - ${window.MTlogo ? window.MTlogo.html(t.description || t.counterparty || '', 18) : ''}${esc(t.description || t.counterparty || 'โ€”')} + ${window.MTlogo ? window.MTlogo.html(t.description || t.counterparty || '', 18, {flowType: t.flow_type}) : ''}${esc(t.description || t.counterparty || 'โ€”')} ${esc(t.bank)} ${MT.acctShort(t.doc_type)}${t.last4 ? ' ยทยท' + t.last4 : ''} ${FLOW_LABEL[t.flow_type] || t.flow_type} ${t.direction === 'credit' ? '+' : 'โˆ’'}${CLP(t.amount)}`).join(''); diff --git a/web/logos.js b/web/logos.js index a38bda7..084164c 100644 --- a/web/logos.js +++ b/web/logos.js @@ -76,16 +76,46 @@ const ch=(m||desc||'?').replace(/[^A-Za-z0-9]/g,'').charAt(0).toUpperCase(); return ch||'?'; } - // returns an HTML string: a brand logo with initial fallback, or just the initial chip - function html(desc, size){ + // ---- category fallback via Twemoji (jdecked fork) ---- + const TW = 'https://cdn.jsdelivr.net/gh/jdecked/twemoji@15.1.0/assets/svg/'; + const CATEGORY_ICON = { + 'Debt & loans':'1f4b3','Cash withdrawals':'1f3e7','Utilities & bills':'1f4a1', + 'Groceries':'1f6d2','Food & dining':'1f354','Fuel & transport':'26fd', + 'Health & pharmacy':'1f48a','Subscriptions & web':'1f4bb','Shopping & retail':'1f6cd', + 'Government & docs':'1f3db','Wallets & online':'1f4f1','Fees & interest':'1f3e6', + 'Other purchases':'1f9fe' + }; + const FLOW_ICON = { income:'1f4b0', inter_in:'1f4b8', inter_out:'1f4b8', + self_transfer:'1f501', card_payment:'1f4b3', credit_line:'1f3e6', fee:'1f3e6' }; + function emojiImg(cp, size){ + const inner = Math.round(size*0.62); + return ''; + } + function categoryFor(desc, opts){ + opts = opts||{}; + if(opts.flowType && opts.flowType!=='expense' && FLOW_ICON[opts.flowType]) return FLOW_ICON[opts.flowType]; + let cat = opts.category; + if(!cat && window.MT && window.MT.spendCategory) cat = window.MT.spendCategory(desc); + if(cat && CATEGORY_ICON[cat]) return CATEGORY_ICON[cat]; + return null; + } + function initChip(desc, size){ + return ''; + } + // returns an HTML string: brand logo, else category emoji, else coloured initial. + function html(desc, size, opts){ size = size||22; const b = brandOf(desc); - const key = b ? b.domain : (desc||''); - const init = b ? b.name.charAt(0).toUpperCase() : initialOf(desc); - const fb = ''; - if(!b) return fb; - const url='https://cdn.brandfetch.io/'+b.domain+'/w/'+(size*2)+'/h/'+(size*2)+'?c='+CLIENT_ID; - return ''; + if(b){ + const url='https://cdn.brandfetch.io/'+b.domain+'/w/'+(size*2)+'/h/'+(size*2)+'?c='+CLIENT_ID; + const cp = categoryFor(desc, opts); + const fb = cp ? emojiImg(cp, size) : initChip(desc, size); + return ''; + } + const cp = categoryFor(desc, opts); + if(cp) return emojiImg(cp, size); + return initChip(desc, size); } window.MTlogo = { html, brandOf }; })();