/* 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 ( { setAccount({ email: email, ok: true }); setAuthed(true); }} /> ); } return (

Tapa do Dia

Mulheres Milionárias

{tapa ? (
{typeMeta.tag || ''} {tapa.gen ? ✦ ao vivo : do baralho}
{tapa.t}
{tapa.a && {tapa.a}}
) : (
Pronta?

Aperte o botão e leve o seu tapa do dia.

Sem filtro de coach. Só verdade com dente.

)}
{tapa && ( )}
setTweak('theme', v)} /> setTweak('accent', v)} /> setTweak('quoteFont', v)} /> setTweak('slap', v)} />
); } const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{ "theme": "claro", "accent": "#d6b87a", "quoteFont": "display", "slap": "medio" }/*EDITMODE-END*/; /* ---------------------------------------------------------------- styles */ const APP_CSS = ` .stage{ position:relative; min-height:100dvh; width:100%; display:flex; flex-direction:column; align-items:center; padding:clamp(20px,5vh,52px) 22px calc(env(safe-area-inset-bottom) + 26px); gap:clamp(18px,3vh,30px); isolation:isolate; } .bg-glow{ position:fixed; inset:0; z-index:-2; pointer-events:none; background: radial-gradient(60% 48% at 50% 38%, var(--accent-glow), transparent 70%), linear-gradient(180deg, var(--bg-2), var(--bg) 60%); } .grain{ position:fixed; inset:0; z-index:-1; pointer-events:none; opacity:var(--grain); background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.8' numOctaves='2'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E"); mix-blend-mode:overlay; } /* masthead */ .masthead{ text-align:center; display:flex; flex-direction:column; align-items:center; gap:6px; } .mark-rule{ width:38px; height:2px; background:var(--accent); margin-bottom:10px; border-radius:2px; } .wordmark{ font-family:var(--font-quote); font-weight:700; line-height:.9; font-size:clamp(30px,8vw,46px); letter-spacing:-0.03em; color:var(--ink); } .subwordmark{ font-family:var(--font-mono); text-transform:uppercase; letter-spacing:.42em; font-size:clamp(9px,2.6vw,11px); color:var(--accent); padding-left:.42em; } /* filters */ .filters{ display:flex; flex-wrap:wrap; justify-content:center; gap:8px; max-width:560px; } .chip{ font-family:var(--font-mono); font-size:11px; letter-spacing:.04em; text-transform:uppercase; color:var(--ink-soft); background:transparent; border:1px solid var(--hairline); border-radius:999px; padding:7px 13px; cursor:pointer; transition:all .18s ease; white-space:nowrap; } .chip:hover{ color:var(--ink); border-color:var(--accent); } .chip-on{ color:var(--accent-ink); background:var(--accent); border-color:var(--accent); } /* arena + card */ .arena{ flex:1; width:100%; max-width:620px; display:flex; align-items:center; justify-content:center; min-height:0; } .card{ position:relative; width:100%; background:linear-gradient(180deg,var(--surface-2),var(--surface)); border:1px solid var(--hairline); border-radius:22px; padding:clamp(28px,6vw,52px) clamp(24px,6vw,48px) clamp(30px,6vw,50px); box-shadow:var(--card-shadow); display:flex; flex-direction:column; gap:22px; overflow:hidden; } .card::before{ content:""; position:absolute; left:0; top:0; bottom:0; width:3px; background:var(--accent); opacity:.9; } .card-top{ display:flex; align-items:center; justify-content:space-between; gap:12px; } .type-tag{ font-family:var(--font-mono); text-transform:uppercase; font-size:10px; letter-spacing:.26em; color:var(--accent); } .src{ font-family:var(--font-mono); font-size:9.5px; letter-spacing:.16em; text-transform:uppercase; color:var(--ink-faint); } .src-ai{ color:var(--accent); animation:pulse-dot 2.6s ease-in-out infinite; } .tapa-quote{ font-family:var(--font-quote); font-weight:var(--quote-weight); font-style:var(--quote-style); letter-spacing:var(--quote-tracking); line-height:var(--quote-lh); font-size:clamp(27px,6.6vw,46px); color:var(--ink); text-wrap:balance; animation:quote-in .5s cubic-bezier(.2,.7,.2,1) both; } .tapa-author{ font-family:var(--font-mono); font-style:normal; font-size:13px; letter-spacing:.04em; color:var(--ink-soft); animation:quote-in .55s .08s cubic-bezier(.2,.7,.2,1) both; } .tapa-author::before{ content:"— "; color:var(--accent); } /* empty state */ .card-empty{ gap:14px; } .empty-kicker{ color:var(--ink-faint); } .empty-line{ font-family:var(--font-quote); font-weight:700; letter-spacing:-0.02em; line-height:1.06; font-size:clamp(26px,6vw,40px); color:var(--ink); text-wrap:balance; } .empty-sub{ font-family:var(--font-mono); font-size:12px; color:var(--ink-faint); letter-spacing:.02em; } /* controls */ .controls{ display:flex; flex-wrap:wrap; gap:10px; justify-content:center; width:100%; max-width:620px; } .btn{ font-family:var(--font-quote); font-weight:600; font-size:clamp(15px,3.6vw,17px); letter-spacing:-0.01em; border-radius:999px; padding:15px 30px; cursor:pointer; transition:transform .12s ease, background .18s ease, color .18s ease, border-color .18s ease; border:1px solid transparent; } .btn:active{ transform:translateY(1px) scale(.985); } .btn-primary{ background:var(--accent); color:var(--accent-ink); box-shadow:0 12px 32px -12px var(--accent-glow); flex:1 1 auto; min-width:200px; } .btn-primary:hover{ filter:brightness(1.06); } .btn-ghost{ background:transparent; color:var(--ink-soft); border-color:var(--hairline); } .btn-ghost:hover{ color:var(--ink); border-color:var(--accent); } /* meta */ .meta{ display:flex; align-items:center; gap:10px; flex-wrap:wrap; justify-content:center; font-family:var(--font-mono); font-size:11px; letter-spacing:.06em; color:var(--ink-faint); text-transform:uppercase; } .counter{ color:var(--ink-soft); } .meta-dot{ opacity:.5; } .logout-btn{ font-family:var(--font-mono); font-size:11px; letter-spacing:.06em; text-transform:uppercase; color:var(--ink-faint); background:transparent; border:0; cursor:pointer; padding:0; transition:color .18s; } .logout-btn:hover{ color:var(--accent); } .gen-status{ display:inline-flex; align-items:center; gap:7px; } .gen-status::before{ content:""; width:6px; height:6px; border-radius:50%; background:var(--ink-faint); } .gen-on{ color:var(--accent); } .gen-on::before{ background:var(--accent); animation:pulse-dot 1s ease-in-out infinite; } @media (max-width:430px){ .btn-ghost{ flex:1 1 100%; } } `; ReactDOM.createRoot(document.getElementById('root')).render();