// Shared localStorage-backed store — powers both the public site and the admin panel.
const STORE_KEY = 'aa-studio-v2';
const DEFAULT_STATE = {
products: [
{id:'p1', cat:'Tratamento Capilar', name:'Shampoo Restaurador Vinho', priceN:168, tone:'wine', style:0, tag:'Best-seller', tagClass:'', img:null, stock:24, desc:'Limpeza suave com reconstrução de queratina. Para cabelos coloridos.'},
{id:'p2', cat:'Óleo Essencial', name:'Óleo Relaxante Bem-Estar', priceN:142, tone:'amber', style:0, tag:'Novo', tagClass:'gold', img:null, stock:18, desc:'Blend de lavanda, gerânio e bergamota para massagem corporal.'},
{id:'p3', cat:'Máscara Hidratação', name:'Máscara Reconstrução Couture', priceN:198, tone:'cream', style:1, tag:null, tagClass:'', img:null, stock:12, desc:'Tratamento intensivo semanal. Resultado de salão em casa.'},
{id:'p4', cat:'Aromaterapia', name:'Sérum Aromático Noite', priceN:224, tone:'rose', style:1, tag:'Edição', tagClass:'gold', img:null, stock:8, desc:'Ritual noturno de relaxamento. Aplicar nos pulsos e nuca.'},
],
gallery: [
{id:'g1', src:'assets/angella-studio.png', caption:'Bastidores do studio', cat:'Studio'},
{id:'g2', src:'assets/angella-portrait.png', caption:'Angella no atendimento', cat:'Equipe'},
{id:'g3', src:'assets/aline-portrait.png', caption:'Aline Maria', cat:'Equipe'},
],
profiles: {
angella: {
name:'Angella Barros', role:'Sócia · Beleza Capilar', photo:'assets/angella-portrait.png',
bio:"Especialista em coloração avançada e cortes autorais. Formada pela L'Oréal Professionnel, com passagens por Madrid e São Paulo. Acredita que cada fio carrega uma história — o cuidado começa pela escuta.",
s1:'12+', s1l:'anos de carreira', s2:'3', s2l:'certificações intl.', objectPosition:'center 18%',
},
aline: {
name:'Aline Maria', role:'Sócia · Massoterapia & Bem-estar', photo:'assets/aline-portrait.png',
bio:'Terapeuta corporal com formação em drenagem linfática método Vodder e massagem ayurvédica. Conduz cada sessão como um ritual — silêncio, respiração e toque firme.',
s1:'9+', s1l:'anos de prática', s2:'5', s2l:'técnicas dominadas', objectPosition:'center 20%',
},
},
hero: { photo:'assets/angella-portrait.png' },
wellness: {
kicker:'Massoterapia & Bem-estar',
title:'O corpo também
pede escuta.',
lede:'Com Aline Maria, dentro do Angella Barros Studio de Beleza. Sessões conduzidas em cabine privada, com técnica, silêncio e tempo — em Parintins.',
listKicker:'Sessões',
listTitle:'Quatro caminhos para o mesmo descanso.',
sessions: [
{id:'relaxante', name:'Massagem Relaxante', time:'60 min', price:'R$ 220', body:'Toque envolvente e ritmo lento para soltar a tensão do dia. Pressão ajustada durante toda a sessão.'},
{id:'drenagem', name:'Drenagem Linfática', time:'75 min', price:'R$ 280', body:'Método Vodder. Movimentos suaves que estimulam a circulação, reduzem inchaço e sensação de peso.'},
{id:'modeladora', name:'Massagem Modeladora', time:'60 min', price:'R$ 260', body:'Manobras firmes e profundas para trabalhar áreas específicas. Indicada em protocolo de sessões.'},
{id:'pedras', name:'Pedras Quentes', time:'80 min', price:'R$ 320', body:'Calor das pedras basálticas sobre os pontos de tensão. Silêncio, respiração e temperatura constante.'},
],
careKicker:'Como funciona',
careTitle:'O cuidado começa antes de você deitar na maca.',
care: [
{k:'Antes', v:'Evite refeições pesadas na hora anterior. Chegue 10 minutos antes para respirar e desacelerar.'},
{k:'Durante', v:'A pressão é ajustada a qualquer momento. Fale sempre que quiser mais leve ou mais firme.'},
{k:'Depois', v:'Beba água, evite esforço no restante do dia. O efeito continua nas horas seguintes.'},
],
},
cinema: {
copyV: 3,
count: 0, prefix:'f-', pad:4, ext:'.jpg', dir:'assets/frames/',
kicker:'Studio de Beleza · Parintins, Amazonas',
title:'Onde a beleza encontra o bem-estar.',
l1:'Cabelo, corpo e presença — no mesmo lugar.',
l2:'Antes da técnica, a escuta. É dela que vem o resultado.',
l3:'Um tempo que é só seu.',
},
instagram: {
handle:'@angellabarrostudio',
url:'https://www.instagram.com/angellabarrostudio',
avatar:'assets/angella-portrait.png',
posts:'428', followers:'12,4 mil', following:'891',
bio:'Studio de Beleza & Bem-estar ✧ Parintins–AM\nColoração autoral · Massoterapia · Loja exclusiva\nAgende pelo site ou WhatsApp.',
sync: {
enabled:false, // liga a atualização automática
token:'', // token da Instagram Graph API
userId:'me',
proxy:'', // endpoint do seu servidor (recomendado em produção)
limit:9, everyMin:60,
lastSync:null, lastError:'',
},
feed: [
{id:'i1', src:'assets/angella-studio.png', caption:'Dia de bastidores no studio — luz boa, música baixa e café.', likes:'842', comments:'37', type:'carousel', tags:['#angellabarrosstudio','#parintins','#bastidores']},
{id:'i2', src:'assets/angella-portrait.png', caption:'Loira iluminada com fundo quente. Trabalho de 3 horas em uma só sessão.', likes:'1.204', comments:'96', type:'photo', tags:['#coloracao','#loiroiluminado']},
{id:'i3', src:'assets/aline-portrait.png', caption:'Aline conduzindo a drenagem — o toque que refaz a semana.', likes:'611', comments:'24', type:'reel', tags:['#massoterapia','#bemestar']},
{id:'i4', src:'assets/angella-studio.png', caption:'Chegou a nova linha de óleos essenciais. Estoque limitado.', likes:'389', comments:'18', type:'photo', tags:['#lojaexclusiva']},
{id:'i5', src:'assets/angella-portrait.png', caption:'Antes e depois de reconstrução capilar. Deslize para ver →', likes:'957', comments:'71', type:'carousel', tags:['#reconstrucao','#antesedepois']},
{id:'i6', src:'assets/aline-portrait.png', caption:'Ritual de pedras quentes: 80 minutos de silêncio absoluto.', likes:'523', comments:'29', type:'reel', tags:['#pedrasquentes','#ritual']},
],
},
settings: {
account: {user:'angella', pass:'studio2018', name:'Angella Barros', email:'contato@angellabarros.com.br'},
pix: {enabled:true, key:'', keyType:'telefone', merchant:'ANGELLA BARROS STUDIO', city:'PARINTINS'},
card: {enabled:true, gateway:'mercadopago', publicKey:'', maxInstallments:6, minInstallment:30, feeNote:'Parcelamento sem juros até 3x'},
delivery: {pickup:true, local:true, localFee:15, freeAbove:250, address:'Centro · Parintins — Amazonas'},
contact: {whatsapp:'5592994794991', email:'contato@angellabarros.com.br'},
},
orders: [],
bookings: [
{id:'b0', client:'Marina Duarte', service:'Coloração Premium', pro:'Angella Barros', date:'2026-08-04', time:'14:30', price:480, status:'confirmado', via:'site'},
{id:'b1', client:'Júlia Ferraz', service:'Drenagem Linfática', pro:'Aline Maria', date:'2026-08-05', time:'10:00', price:280, status:'pendente', via:'ia'},
],
};
function loadStore() {
try {
const raw = localStorage.getItem(STORE_KEY);
if (raw) {
const parsed = JSON.parse(raw);
const igSaved = parsed.instagram || {};
const cinemaSaved = parsed.cinema || {};
const cinema = cinemaSaved.copyV === DEFAULT_STATE.cinema.copyV
? {...DEFAULT_STATE.cinema, ...cinemaSaved}
: {...DEFAULT_STATE.cinema, count:cinemaSaved.count ?? 0, prefix:cinemaSaved.prefix ?? 'f-', pad:cinemaSaved.pad ?? 4, ext:cinemaSaved.ext ?? '.jpg', dir:cinemaSaved.dir ?? 'assets/frames/'};
const st = parsed.settings || {};
const merged = {...DEFAULT_STATE, ...parsed,
profiles:{...DEFAULT_STATE.profiles, ...(parsed.profiles||{})},
instagram:{...DEFAULT_STATE.instagram, ...igSaved, sync:{...DEFAULT_STATE.instagram.sync, ...(igSaved.sync||{})}},
cinema,
wellness:{...DEFAULT_STATE.wellness, ...(parsed.wellness||{})},
settings:{
account:{...DEFAULT_STATE.settings.account, ...(st.account||{})},
pix:{...DEFAULT_STATE.settings.pix, ...(st.pix||{})},
card:{...DEFAULT_STATE.settings.card, ...(st.card||{})},
delivery:{...DEFAULT_STATE.settings.delivery, ...(st.delivery||{})},
contact:{...DEFAULT_STATE.settings.contact, ...(st.contact||{})},
}};
// migrações de dados de exemplo para os dados reais do studio
if (!merged.settings.contact.whatsapp || merged.settings.contact.whatsapp === '5592000000000')
merged.settings.contact.whatsapp = DEFAULT_STATE.settings.contact.whatsapp;
if (merged.instagram.handle === '@angellabarros.studio') {
merged.instagram.handle = DEFAULT_STATE.instagram.handle;
merged.instagram.url = DEFAULT_STATE.instagram.url;
}
return merged;
}
} catch (e) { console.warn('store load failed', e); }
return DEFAULT_STATE;
}
let _state = loadStore();
const _subs = new Set();
function setStore(patch) {
const next = typeof patch === 'function' ? patch(_state) : patch;
_state = {..._state, ...next};
try { localStorage.setItem(STORE_KEY, JSON.stringify(_state)); }
catch (e) { console.warn('store save failed (quota?)', e); }
_subs.forEach(fn => fn(_state));
}
function resetStore() {
try { localStorage.removeItem(STORE_KEY); } catch (e) {}
_state = DEFAULT_STATE;
_subs.forEach(fn => fn(_state));
}
function useStore() {
const [s, set] = React.useState(_state);
React.useEffect(() => {
const fn = (x) => set(x);
_subs.add(fn);
set(_state);
return () => { _subs.delete(fn); };
}, []);
return s;
}
// Downscale + compress an uploaded image so localStorage doesn't blow its quota.
function fileToDataURL(file, max = 1100, quality = 0.82) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onerror = reject;
reader.onload = () => {
const img = new Image();
img.onerror = reject;
img.onload = () => {
let {width:w, height:h} = img;
const scale = Math.min(1, max / Math.max(w, h));
w = Math.round(w * scale); h = Math.round(h * scale);
const c = document.createElement('canvas');
c.width = w; c.height = h;
const ctx = c.getContext('2d');
ctx.fillStyle = '#F2ECE5'; ctx.fillRect(0,0,w,h);
ctx.drawImage(img, 0, 0, w, h);
resolve(c.toDataURL('image/jpeg', quality));
};
img.src = reader.result;
};
reader.readAsDataURL(file);
});
}
const uid = (p='x') => p + Math.random().toString(36).slice(2,8);
const brl = (n) => 'R$ ' + Number(n||0).toLocaleString('pt-BR', {maximumFractionDigits:0});
const brlc = (n) => 'R$ ' + Number(n||0).toLocaleString('pt-BR', {minimumFractionDigits:2, maximumFractionDigits:2});
/* ——— Pix “copia e cola” (BR Code EMV, padrão Banco Central) ———
Gera um payload estático real: basta a Angella cadastrar a chave Pix dela no painel. */
function crc16(str) {
let crc = 0xFFFF;
for (let i = 0; i < str.length; i++) {
crc ^= str.charCodeAt(i) << 8;
for (let b = 0; b < 8; b++) crc = (crc & 0x8000) ? ((crc << 1) ^ 0x1021) & 0xFFFF : (crc << 1) & 0xFFFF;
}
return crc.toString(16).toUpperCase().padStart(4, '0');
}
const emv = (id, v) => id + String(v.length).padStart(2, '0') + v;
const sanitize = (s, max) => (s||'').normalize('NFD').replace(/[\u0300-\u036f]/g,'').replace(/[^A-Za-z0-9 .\-]/g,'').trim().toUpperCase().slice(0, max);
function pixPayload({key, merchant, city, amount, txid}) {
if (!key) return '';
const account = emv('00','BR.GOV.BCB.PIX') + emv('01', key.trim());
let p = emv('00','01') + emv('01','11') + emv('26', account) + emv('52','0000') + emv('53','986');
if (amount > 0) p += emv('54', Number(amount).toFixed(2));
p += emv('58','BR') + emv('59', sanitize(merchant,25) || 'STUDIO') + emv('60', sanitize(city,15) || 'PARINTINS');
p += emv('62', emv('05', (txid||'***').replace(/[^A-Za-z0-9]/g,'').slice(0,25) || '***'));
p += '6304';
return p + crc16(p);
}
// Número do pedido legível: AB-260812-4F2K
function orderCode() {
const d = new Date();
const ymd = String(d.getFullYear()).slice(2) + String(d.getMonth()+1).padStart(2,'0') + String(d.getDate()).padStart(2,'0');
return 'AB-' + ymd + '-' + Math.random().toString(36).slice(2,6).toUpperCase();
}
Object.assign(window, { useStore, setStore, resetStore, fileToDataURL, uid, brl, brlc, pixPayload, orderCode, DEFAULT_STATE });