kua-money-trace/web/logos.js

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=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[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(&quot;data-fb&quot;)">';
}
const cp = categoryFor(desc, opts);
if(cp) return emojiImg(cp, size);
return initChip(desc, size);
}
window.MTlogo = { html, brandOf };
})();