Twemoji category icons as logo fallback
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).
This commit is contained in:
parent
e0f2252124
commit
e4e8f20555
|
|
@ -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; }
|
||||
|
|
|
|||
|
|
@ -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 = `<span class="m-rank">${i + 1}</span><span class="m-name">${logo}<span>${esc(name)}<span class="m-cat">${esc(cat)}</span></span></span><span class="m-cnt">${d.txns.length}×</span><span class="m-amt">${window.MT.CLPk(d.value)}</span>`;
|
||||
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 `<div class="bk-row">
|
||||
<span class="bk-row-desc with-logo">${blogo}<span>${esc(desc)}</span></span>
|
||||
<span class="bk-row-cnt">${d.txns.length}×</span>
|
||||
|
|
|
|||
|
|
@ -285,7 +285,7 @@
|
|||
const shown = rows.slice(0, st.limit);
|
||||
$('#lx-rows').innerHTML = shown.map(t => `
|
||||
<tr><td class="lx-date">${t.date}</td>
|
||||
<td class="lx-desc"><span class="lx-logo-wrap">${window.MTlogo ? window.MTlogo.html(t.description || t.counterparty || '', 18) : ''}<span>${esc(t.description || t.counterparty || '—')}</span></span></td>
|
||||
<td class="lx-desc"><span class="lx-logo-wrap">${window.MTlogo ? window.MTlogo.html(t.description || t.counterparty || '', 18, {flowType: t.flow_type}) : ''}<span>${esc(t.description || t.counterparty || '—')}</span></span></td>
|
||||
<td style="white-space:nowrap;color:var(--ink-mute)">${esc(t.bank)} <span class="lx-pdf">${MT.acctShort(t.doc_type)}${t.last4 ? ' ··' + t.last4 : ''}</span></td>
|
||||
<td><span class="lx-flow-tag"><i style="background:${FLOW_C[t.flow_type] || col.mute()}"></i>${FLOW_LABEL[t.flow_type] || t.flow_type}</span></td>
|
||||
<td class="r lx-amt ${t.direction === 'credit' ? 'cr' : 'db'}">${t.direction === 'credit' ? '+' : '−'}${CLP(t.amount)}</td></tr>`).join('');
|
||||
|
|
|
|||
42
web/logos.js
42
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 <img> 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 '<span class="mlogo mlogo-cat" style="width:'+size+'px;height:'+size+'px">'+
|
||||
'<img src="'+TW+cp+'.svg" width="'+inner+'" height="'+inner+'" loading="lazy" alt=""></span>';
|
||||
}
|
||||
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 '<span class="mlogo mlogo-fb" style="width:'+size+'px;height:'+size+'px;background:'+colorOf(desc||'')+'">'+esc(initialOf(desc))+'</span>';
|
||||
}
|
||||
// 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 = '<span class="mlogo mlogo-fb" style="width:'+size+'px;height:'+size+'px;background:'+colorOf(key)+'">'+esc(init)+'</span>';
|
||||
if(!b) return fb;
|
||||
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 '<img class="mlogo" src="'+url+'" width="'+size+'" height="'+size+'" loading="lazy" alt="'+esc(b.name)+'" title="'+esc(b.name)+'" data-fb="'+esc(fb)+'" onerror="this.outerHTML=this.getAttribute("data-fb")">';
|
||||
}
|
||||
const cp = categoryFor(desc, opts);
|
||||
if(cp) return emojiImg(cp, size);
|
||||
return initChip(desc, size);
|
||||
}
|
||||
window.MTlogo = { html, brandOf };
|
||||
})();
|
||||
|
|
|
|||
Loading…
Reference in New Issue