122 lines
5.7 KiB
JavaScript
122 lines
5.7 KiB
JavaScript
/* money-trace · merchant logos via Brandfetch CDN.
|
|
High-precision alias map (description regex -> brand domain). We only show a
|
|
logo when a description confidently maps to a known brand; everything else
|
|
falls back to a coloured initial. No guessing of arbitrary websites. */
|
|
(function(){
|
|
'use strict';
|
|
const CLIENT_ID = '1idZRzFTMDBLO5z-dbd';
|
|
// [regex on UPPERCASED description, brand name, domain]
|
|
const ALIAS = [
|
|
[/UBER\s*EATS|UBR.*EATS|PS UBER EATS/, 'Uber Eats','ubereats.com'],
|
|
[/\bUBER\b|\bUBR\b/, 'Uber','uber.com'],
|
|
[/RAPPI/, 'Rappi','rappi.com'],
|
|
[/MERCADO ?PAGO|MERPAGO|\bMP\b/, 'Mercado Pago','mercadopago.cl'],
|
|
[/APPLE/, 'Apple','apple.com'],
|
|
[/NETFLIX/, 'Netflix','netflix.com'],
|
|
[/SPOTIFY/, 'Spotify','spotify.com'],
|
|
[/\bTEMU\b/, 'Temu','temu.com'],
|
|
[/ALIEXPRESS/, 'AliExpress','aliexpress.com'],
|
|
[/\bAMAZON\b/, 'Amazon','amazon.com'],
|
|
[/\bSHEIN\b/, 'Shein','shein.com'],
|
|
[/CABIFY/, 'Cabify','cabify.com'],
|
|
[/\bDIDI\b/, 'DiDi','didiglobal.com'],
|
|
[/PAYPAL/, 'PayPal','paypal.com'],
|
|
[/PAYSEND/, 'Paysend','paysend.com'],
|
|
[/\bMC ?DONALD/, 'McDonalds','mcdonalds.com'],
|
|
[/\bKFC\b/, 'KFC','kfc.com'],
|
|
[/STA ISABEL|SANTA ISABEL/, 'Santa Isabel','santaisabel.cl'],
|
|
[/\bJUMBO\b/, 'Jumbo','jumbo.cl'],
|
|
[/HIP ?LIDER|\bLIDER\b/, 'Lider','lider.cl'],
|
|
[/TOTTUS/, 'Tottus','tottus.cl'],
|
|
[/UNIMARC/, 'Unimarc','unimarc.cl'],
|
|
[/\bCOPEC\b/, 'Copec','copec.cl'],
|
|
[/\bSHELL\b/, 'Shell','shell.com'],
|
|
[/FALABELLA/, 'Falabella','falabella.com'],
|
|
[/RIPLEY/, 'Ripley','ripley.cl'],
|
|
[/SODIMAC/, 'Sodimac','sodimac.cl'],
|
|
[/\bPARIS\b/, 'Paris','paris.cl'],
|
|
[/\bENTEL\b/, 'Entel','entel.cl'],
|
|
[/\bWOM\b/, 'WOM','wom.cl'],
|
|
[/MOVISTAR/, 'Movistar','movistar.cl'],
|
|
[/\bCLARO\b/, 'Claro','claro.cl'],
|
|
[/TURBUS|TUR BUS/, 'Turbus','turbus.cl'],
|
|
[/CRUZ VERDE/, 'Cruz Verde','cruzverde.cl'],
|
|
[/SALCOBRAND/, 'Salcobrand','salcobrand.cl'],
|
|
[/AHUMADA/, 'Farmacias Ahumada','farmaciasahumada.cl'],
|
|
[/INTEGRAMEDICA/, 'IntegraMedica','integramedica.cl'],
|
|
[/\bGOOGLE\b/, 'Google','google.com'],
|
|
[/MICROSOFT/, 'Microsoft','microsoft.com'],
|
|
[/\bOPENAI\b/, 'OpenAI','openai.com'],
|
|
[/ANTHROPIC/, 'Anthropic','anthropic.com'],
|
|
[/HETZNER/, 'Hetzner','hetzner.com'],
|
|
[/SITEGROUND/, 'SiteGround','siteground.com'],
|
|
[/ADIDAS/, 'adidas','adidas.com'],
|
|
[/NEW BALANCE/, 'New Balance','newbalance.com'],
|
|
[/\bENEL\b/, 'Enel','enel.cl'],
|
|
[/SERVIPAG/, 'Servipag','servipag.com'],
|
|
[/SENCILLITO/, 'Sencillito','sencillito.com'],
|
|
[/\bSKY\b/, 'Sky Airline','skyairline.com'],
|
|
[/LATAM/, 'LATAM','latamairlines.com'],
|
|
];
|
|
const PALETTE = ['#5b8def','#e0729a','#6fb86f','#d9a14b','#9b7fd4','#54b3c4','#cf7d56','#7d8aa0'];
|
|
const COLORS = new Map();
|
|
function colorOf(key){
|
|
if(COLORS.has(key)) return COLORS.get(key);
|
|
let h=0; for(let i=0;i<key.length;i++) h=(h*31+key.charCodeAt(i))>>>0;
|
|
const c=PALETTE[h%PALETTE.length]; COLORS.set(key,c); return c;
|
|
}
|
|
const esc = s => String(s||'').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
|
function brandOf(desc){
|
|
const s=(desc||'').toUpperCase();
|
|
for(const [re,name,domain] of ALIAS) if(re.test(s)) return {name,domain};
|
|
return null;
|
|
}
|
|
function initialOf(desc){
|
|
const m=(desc||'').replace(/^(PS|PAGO:?|MERPAGO|PAYU|MP|DL|CV|COMPRA|PREPAGO)\b[\s\*:—-]*/i,'').trim();
|
|
const ch=(m||desc||'?').replace(/[^A-Za-z0-9]/g,'').charAt(0).toUpperCase();
|
|
return ch||'?';
|
|
}
|
|
// ---- 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);
|
|
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 };
|
|
})();
|