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; }
|
.mrow .m-name { display: flex; align-items: center; gap: 9px; }
|
||||||
.bk-row-desc.with-logo { display: flex; align-items: center; gap: 8px; }
|
.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; }
|
.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) => {
|
merch.forEach(([name, d], i) => {
|
||||||
const cat = window.MT.spendCategory(d.txns[0].description);
|
const cat = window.MT.spendCategory(d.txns[0].description);
|
||||||
const row = document.createElement('div'); row.className = 'mrow';
|
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.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('mousemove', e => window.MTtip.rows(e, name, col.spend(), d.txns));
|
||||||
row.addEventListener('mouseleave', () => window.MTtip.hide());
|
row.addEventListener('mouseleave', () => window.MTtip.hide());
|
||||||
|
|
@ -180,7 +180,7 @@
|
||||||
section.className = 'bk-section';
|
section.className = 'bk-section';
|
||||||
const rowsHtml = descs.slice(0, 30).map(([desc, d]) => {
|
const rowsHtml = descs.slice(0, 30).map(([desc, d]) => {
|
||||||
const cpct = Math.round(d.value / catData.value * 100);
|
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">
|
return `<div class="bk-row">
|
||||||
<span class="bk-row-desc with-logo">${blogo}<span>${esc(desc)}</span></span>
|
<span class="bk-row-desc with-logo">${blogo}<span>${esc(desc)}</span></span>
|
||||||
<span class="bk-row-cnt">${d.txns.length}×</span>
|
<span class="bk-row-cnt">${d.txns.length}×</span>
|
||||||
|
|
|
||||||
|
|
@ -285,7 +285,7 @@
|
||||||
const shown = rows.slice(0, st.limit);
|
const shown = rows.slice(0, st.limit);
|
||||||
$('#lx-rows').innerHTML = shown.map(t => `
|
$('#lx-rows').innerHTML = shown.map(t => `
|
||||||
<tr><td class="lx-date">${t.date}</td>
|
<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 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><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('');
|
<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();
|
const ch=(m||desc||'?').replace(/[^A-Za-z0-9]/g,'').charAt(0).toUpperCase();
|
||||||
return ch||'?';
|
return ch||'?';
|
||||||
}
|
}
|
||||||
// returns an HTML string: a brand logo <img> with initial fallback, or just the initial chip
|
// ---- category fallback via Twemoji (jdecked fork) ----
|
||||||
function html(desc, size){
|
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;
|
size = size||22;
|
||||||
const b = brandOf(desc);
|
const b = brandOf(desc);
|
||||||
const key = b ? b.domain : (desc||'');
|
if(b){
|
||||||
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;
|
|
||||||
const url='https://cdn.brandfetch.io/'+b.domain+'/w/'+(size*2)+'/h/'+(size*2)+'?c='+CLIENT_ID;
|
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")">';
|
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 };
|
window.MTlogo = { html, brandOf };
|
||||||
})();
|
})();
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue