{tapa.t}{tapa.a && {tapa.a}}
Aperte o botão e leve o seu tapa do dia.
Sem filtro de coach. Só verdade com dente.
/* Tapa do Dia — Mulheres Milionárias
App principal: gerador ao vivo (Claude API) + banco curado de reserva. */
const { useState, useEffect, useRef, useCallback } = React;
/* ---------------------------------------------------------------- presets */
const THEMES = {
dark: {
'--bg': '#100e0c', '--bg-2': '#16130f', '--surface': '#1c1815', '--surface-2': '#242019',
'--ink': '#f3ece1', '--ink-soft': '#b6ab99', '--ink-faint': '#7c7367',
'--hairline': 'rgba(255,255,255,0.09)',
'--card-shadow': '0 50px 100px -34px rgba(0,0,0,0.85)',
'--grain': '0.05'
},
claro: {
'--bg': '#ece5da', '--bg-2': '#f2ece2', '--surface': '#fbf8f2', '--surface-2': '#ffffff',
'--ink': '#1b1712', '--ink-soft': '#5b5346', '--ink-faint': '#948977',
'--hairline': 'rgba(28,24,19,0.13)',
'--card-shadow': '0 44px 80px -34px rgba(70,52,28,0.30)',
'--grain': '0.025'
}
};
const ACCENTS = {
'#d6b87a': { ink: '#15110b', glow: 'rgba(214,184,122,0.20)' }, // champagne
'#d9a441': { ink: '#1a1305', glow: 'rgba(217,164,65,0.20)' }, // dourado
'#a83248': { ink: '#fff5f6', glow: 'rgba(168,50,72,0.20)' }, // vinho
'#2c8a63': { ink: '#f1fff8', glow: 'rgba(44,138,99,0.20)' }, // esmeralda
'#c8623c': { ink: '#fff4ef', glow: 'rgba(200,98,60,0.20)' } // terracota
};
const QUOTE_FONTS = {
display: { family: "'Bricolage Grotesque', sans-serif", weight: 700, style: 'normal', tracking: '-0.02em', lh: 1.04 },
impacto: { family: "'Archivo', sans-serif", weight: 900, style: 'normal', tracking: '-0.03em', lh: 1.0 },
editorial: { family: "'Newsreader', serif", weight: 500, style: 'italic', tracking: '0', lh: 1.08 }
};
const FILTERS = [
{ key: 'all', label: 'Todos' },
{ key: 'verdade', label: 'Verdade ácida' },
{ key: 'citacao', label: 'Citação' },
{ key: 'sarcasmo', label: 'Sarcasmo' },
{ key: 'riqueza', label: 'Riqueza' },
{ key: 'posicionamento', label: 'Posicionamento' },
{ key: 'fitness', label: 'Fitness' },
{ key: 'religioso', label: 'Religioso' },
{ key: 'motivacional', label: 'Motivacional' },
{ key: 'mentalidade', label: 'Mentalidade' }
];
const TYPE_DESC = {
verdade: 'verdade ácida que todo mundo sabe mas ninguém fala',
citacao: 'citação REAL de alguém reconhecido em negócios, finanças ou filosofia',
sarcasmo: 'sarcasmo empresarial que dói porque é real',
riqueza: 'mentalidade financeira, patrimônio e legado',
posicionamento: 'valor, preço e autoridade',
fitness: 'corpo, disciplina física, treino e saúde tratados como ativo e como espelho da disciplina nos negócios',
religioso: 'fé, propósito e espiritualidade — mas com substância e ação, nunca religiosidade vazia; respeitosa e sem citar versículo específico',
motivacional: 'motivação COM DENTE — empurrão direto pra agir, nada de "acredite em você" genérico',
mentalidade: 'mentalidade milionária: abundância vs escassez, como rica pensa diferente de quebrada'
};
const BANK = window.TAPAS_BANK || [];
const TYPES = window.TAPA_TYPES || {};
const VALID_KEYS = Object.keys(TYPE_DESC);
const AI_OK = !!(window.claude && typeof window.claude.complete === 'function');
const STORE_KEY = 'tapadodia.v1';
/* ---------------------------------------------------------------- helpers */
const norm = (s) => (s || '').toLowerCase().replace(/[^a-z0-9áàâãéêíóôõúüç]+/gi, '');
function parseTapas(raw) {
if (!raw) return [];
let s = String(raw).trim().replace(/```json/gi, '').replace(/```/g, '').trim();
const a = s.indexOf('['), b = s.lastIndexOf(']');
if (a >= 0 && b > a) s = s.slice(a, b + 1);
let arr;
try { arr = JSON.parse(s); } catch (e) { return []; }
if (!Array.isArray(arr)) return [];
return arr
.filter(o => o && typeof o.text === 'string' && o.text.trim().length > 5 && o.text.length < 240)
.map(o => ({
t: o.text.trim(),
a: (o.author && String(o.author).trim()) || null,
k: VALID_KEYS.includes(o.type) ? o.type : 'verdade',
gen: true
}));
}
const PERSONA = `Você é o "Tapa do Dia", um gerador de frases de impacto para MULHERES MILIONÁRIAS — empreendedoras, CEOs, donas do próprio negócio e da própria riqueza.
Cada frase é um TAPA NA CARA: bate na consciência e dói porque é verdade. Pode ser motivacional com dente, sarcástica, filosófica ou uma citação real.
TOM: sarcástico mas inteligente; ironia fina; direto e sem frescura; sempre no universo de dinheiro, negócio, patrimônio, liberdade financeira, posicionamento e legado. Toda frase tem dente.
NUNCA: autoajuda genérica, linguagem de coach de Instagram, "acredite em você", "jornada", "propósito maior", "o universo conspira", nem nada óbvio demais que uma mulher que fatura 7 dígitos ignoraria. Nunca infantilize.
TIPOS (campo "type"): verdade, citacao, sarcasmo, riqueza, posicionamento.
Para "citacao" use SOMENTE citações reais e corretamente atribuídas (author = nome real). Se não tiver certeza absoluta da autoria, use outro tipo e author = null.
Frases curtas e afiadas: 1 a 2 sentenças, no máximo ~30 palavras. Português do Brasil.`;
async function generateBatch(filterKey, avoidList, n = 4) {
const typeLine = filterKey === 'all'
? 'Varie livremente entre os 5 tipos.'
: `Todos do tipo "${filterKey}" (${TYPE_DESC[filterKey]}).`;
const avoid = avoidList && avoidList.length
? `\nNÃO repita nem parafraseie estas ideias já usadas: ${avoidList.map(x => `"${x}"`).join('; ')}.`
: '';
const user = `${PERSONA}
Gere ${n} tapas NOVOS, originais e afiados. ${typeLine}${avoid}
Responda SOMENTE com um array JSON válido, sem nenhum texto fora dele, no formato exato:
[{"text":"...","author":null,"type":"sarcasmo"}]`;
const raw = await window.claude.complete({ messages: [{ role: 'user', content: user }] });
return parseTapas(raw);
}
/* ---------------------------------------------------------------- App */
function loadSaved() {
try {
const s = JSON.parse(localStorage.getItem(STORE_KEY));
if (s && typeof s === 'object') return s;
} catch (e) {}
return null;
}
function App() {
const saved = useRef(loadSaved()).current;
const [t, setTweak] = useTweaks(TWEAK_DEFAULTS);
const [authed, setAuthed] = useState(() => !!(window.authLoad && window.authLoad()));
const [account, setAccount] = useState(() => (window.authLoad && window.authLoad()) || null);
const [tapa, setTapa] = useState(saved && saved.tapa ? saved.tapa : null);
const [filter, setFilter] = useState(saved && saved.filter ? saved.filter : 'all');
const [count, setCount] = useState(saved && saved.count ? saved.count : 0);
const [animKey, setAnimKey] = useState(0);
const [copied, setCopied] = useState(false);
const [genActive, setGenActive] = useState(0);
const seenRef = useRef(new Set(saved && saved.seen ? saved.seen : []));
const bufferRef = useRef(FILTERS.reduce((acc, f) => { acc[f.key] = []; return acc; }, {}));
const fetchingRef = useRef({});
const filterRef = useRef(filter);
filterRef.current = filter;
/* apply theme / accent / fonts to :root */
useEffect(() => {
const root = document.documentElement;
const theme = THEMES[t.theme] || THEMES.dark;
Object.entries(theme).forEach(([k, v]) => root.style.setProperty(k, v));
const acc = ACCENTS[t.accent] || ACCENTS['#d6b87a'];
root.style.setProperty('--accent', t.accent);
root.style.setProperty('--accent-ink', acc.ink);
root.style.setProperty('--accent-glow', acc.glow);
const f = QUOTE_FONTS[t.quoteFont] || QUOTE_FONTS.display;
root.style.setProperty('--font-quote', f.family);
root.style.setProperty('--quote-weight', f.weight);
root.style.setProperty('--quote-style', f.style);
root.style.setProperty('--quote-tracking', f.tracking);
root.style.setProperty('--quote-lh', f.lh);
}, [t.theme, t.accent, t.quoteFont]);
/* persist */
useEffect(() => {
try {
localStorage.setItem(STORE_KEY, JSON.stringify({
tapa, filter, count, seen: Array.from(seenRef.current).slice(-400)
}));
} catch (e) {}
}, [tapa, filter, count]);
const recentSeen = () => Array.from(seenRef.current).slice(-10);
/* avoid-list uses the actual recent texts, not normalised */
const recentTexts = useRef([]);
const prefetch = useCallback(async (key) => {
if (!AI_OK) return;
if (fetchingRef.current[key]) return;
if (bufferRef.current[key].length >= 4) return;
fetchingRef.current[key] = true;
setGenActive(g => g + 1);
try {
const fresh = await generateBatch(key, recentTexts.current.slice(-8));
const unique = [];
const local = new Set(bufferRef.current[key].map(x => norm(x.t)));
for (const item of fresh) {
const nz = norm(item.t);
if (seenRef.current.has(nz) || local.has(nz)) continue;
if (key !== 'all' && item.k !== key) item.k = key;
local.add(nz);
unique.push(item);
}
bufferRef.current[key] = bufferRef.current[key].concat(unique);
} catch (e) {
/* silent — bank fallback covers it */
} finally {
fetchingRef.current[key] = false;
setGenActive(g => Math.max(0, g - 1));
}
}, []);
/* warm the buffer for the active filter */
useEffect(() => {
prefetch(filter);
}, [filter, prefetch]);
function takeFromBuffer(key) {
const buf = bufferRef.current[key];
while (buf.length) {
const item = buf.shift();
if (!seenRef.current.has(norm(item.t))) return item;
}
return null;
}
function bankFallback(key) {
const pool = BANK.filter(x => key === 'all' || x.k === key);
if (!pool.length) return null;
let unseen = pool.filter(x => !seenRef.current.has(norm(x.t)));
if (!unseen.length) {
pool.forEach(x => seenRef.current.delete(norm(x.t)));
unseen = pool.slice();
}
return { ...unseen[Math.floor(Math.random() * unseen.length)] };
}
const deliver = useCallback(() => {
const f = filterRef.current;
let next = takeFromBuffer(f);
if (!next) next = bankFallback(f);
if (!next) return;
seenRef.current.add(norm(next.t));
recentTexts.current.push(next.t);
if (recentTexts.current.length > 16) recentTexts.current.shift();
setTapa(next);
setCount(c => c + 1);
setAnimKey(k => k + 1);
setCopied(false);
prefetch(f); // refill in background
}, [prefetch]);
const copyStory = useCallback(async () => {
if (!tapa) return;
const txt = `"${tapa.t}"${tapa.a ? `\n— ${tapa.a}` : ''}\n\nTapa do Dia · Mulheres Milionárias`;
try {
await navigator.clipboard.writeText(txt);
} catch (e) {
const ta = document.createElement('textarea');
ta.value = txt; document.body.appendChild(ta); ta.select();
try { document.execCommand('copy'); } catch (_) {}
document.body.removeChild(ta);
}
setCopied(true);
setTimeout(() => setCopied(false), 1800);
}, [tapa]);
const typeMeta = tapa ? (TYPES[tapa.k] || {}) : {};
const logout = useCallback(() => {
if (window.authClear) window.authClear();
setAccount(null);
setAuthed(false);
}, []);
/* gate: sem sessão válida, mostra o login com a cara do app */
if (!authed && window.LoginGate) {
const Gate = window.LoginGate;
return (
Mulheres Milionárias
{tapa.t}{tapa.a && {tapa.a}}
Aperte o botão e leve o seu tapa do dia.
Sem filtro de coach. Só verdade com dente.