/* ═══════════════════════════════════════════════════════ TSW PRACTICA — Shadow DOM Framer Injection Paste this as Custom Code → Before in Framer ═══════════════════════════════════════════════════════ */ (function () { 'use strict'; /* ── 1. Create & mount the shadow host ── */ var host = document.createElement('div'); host.id = 'practica-shadow-host'; host.style.cssText = 'position:fixed;inset:0;z-index:9999;overflow-y:auto;'; document.body.appendChild(host); /* ── Guard: re-append if Framer React hydration removes the host ── */ var _hostObserver = new MutationObserver(function () { if (!document.getElementById('practica-shadow-host')) { document.body.appendChild(host); } }); _hostObserver.observe(document.body, { childList: true }); var shadow = host.attachShadow({ mode: 'open' }); /* ── 2. Inject CSS (Google Fonts + all app styles) inside shadow ── */ var styleEl = document.createElement('style'); styleEl.textContent = [ "@import url('https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,400;0,600;0,700;0,800;0,900;1,700&family=Open+Sans:wght@400;500;600&display=swap');", /* Host-level reset so the shadow root itself fills the container */ ":host { display:block; width:100%; min-height:100vh; }", `/* ============================================================ THE SPANISH WEY — PRACTICA 7-Day Speaking Starter Brand: #243757 navy | #febd58 yellow | #de4300 orange | #dad5b7 cream ============================================================ */ :host { --navy: #243757; --navy-dk: #1a2a42; --navy-lt: #2d4a70; --yellow: #febd58; --yellow-lt: #fff5dd; --orange: #de4300; --orange-lt: #fff1eb; --cream: #dad5b7; --cream-lt: #f5f3ee; --cream-bg: #faf8f3; --white: #ffffff; --text: #1c1c2e; --text-mid: #4a4a6a; --text-soft: #888899; --border: #e5e2d8; --title-font: 'Poppins', system-ui, sans-serif; --body-font: 'Open Sans', system-ui, sans-serif; --r: 12px; --r-sm: 8px; --shadow: 0 2px 12px rgba(36,55,87,.09); --shadow-md: 0 4px 20px rgba(36,55,87,.13); --shadow-lg: 0 8px 36px rgba(36,55,87,.18); } *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } html { scroll-behavior: smooth; } body { font-family: var(--body-font); background: var(--cream-bg); color: var(--text); line-height: 1.6; min-height: 100vh; } button { cursor: pointer; font-family: var(--body-font); border: none; } input, textarea { font-family: var(--body-font); } a { color: inherit; } /* ── Screen management ── */ .screen { display: none !important; } .screen.active { display: block !important; } /* ── Loading ── */ #s-loading { position: fixed; inset: 0; display: flex; align-items: center; justify-content: center; background: var(--navy); z-index: 9999; } .ld-inner { text-align: center; } .ld-brand { font-family: var(--title-font); font-size: 1.5rem; font-weight: 900; color: var(--yellow); letter-spacing: .03em; margin-bottom: 1.25rem; } .ld-spin { width: 36px; height: 36px; margin: 0 auto; border: 3px solid rgba(255,255,255,.15); border-top-color: var(--yellow); border-radius: 50%; animation: spin .75s linear infinite; } @keyframes spin { to { transform: rotate(360deg); } } /* ── Login Screen ── */ #s-login { min-height: 100vh; display: none; flex-direction: column; } #s-login.active { display: flex !important; } .lg-hero { background: var(--navy); color: white; padding: 3rem 1.5rem 3.5rem; text-align: center; position: relative; overflow: hidden; } .lg-hero::before { content: ''; position: absolute; top: -80px; right: -80px; width: 220px; height: 220px; background: var(--yellow); opacity: .06; border-radius: 50%; } .lg-hero::after { content: ''; position: absolute; bottom: -50px; left: -60px; width: 180px; height: 180px; background: var(--orange); opacity: .06; border-radius: 50%; } .lg-logo { display: flex; justify-content: center; margin-bottom: 1.25rem; position: relative; z-index: 1; } .lg-logo svg { filter: drop-shadow(0 4px 12px rgba(0,0,0,.25)); } .lg-badge { display: inline-block; background: var(--yellow); color: var(--navy); font-family: var(--title-font); font-size: .72rem; font-weight: 800; letter-spacing: .09em; text-transform: uppercase; padding: 5px 14px; border-radius: 999px; margin-bottom: 1.25rem; position: relative; z-index: 1; } .lg-hero h1 { font-family: var(--title-font); font-size: clamp(1.7rem, 5vw, 2.6rem); font-weight: 900; line-height: 1.15; margin-bottom: .6rem; position: relative; z-index: 1; } .lg-hero h1 em { color: var(--yellow); font-style: normal; } .lg-tagline { font-size: .95rem; opacity: .75; max-width: 400px; margin: 0 auto 2rem; position: relative; z-index: 1; } .lg-stats { display: flex; justify-content: center; gap: 2.5rem; flex-wrap: wrap; position: relative; z-index: 1; } .lg-stat { text-align: center; } .lg-stat-n { display: block; font-family: var(--title-font); font-size: 1.5rem; font-weight: 900; color: var(--yellow); } .lg-stat-l { font-size: .7rem; text-transform: uppercase; letter-spacing: .06em; opacity: .65; } .lg-body { flex: 1; padding: 2rem 1.25rem 3rem; max-width: 480px; margin: 0 auto; width: 100%; } .lg-card { background: white; border-radius: var(--r); padding: 1.75rem; box-shadow: var(--shadow-lg); margin-bottom: 1.25rem; } .lg-card h2 { font-family: var(--title-font); font-size: 1.15rem; font-weight: 700; color: var(--navy); margin-bottom: .35rem; } .lg-card > p { font-size: .88rem; color: var(--text-mid); margin-bottom: 1.25rem; } .form-row { margin-bottom: .9rem; } .form-row label { display: block; font-size: .72rem; font-weight: 700; color: var(--navy); text-transform: uppercase; letter-spacing: .06em; margin-bottom: .35rem; } .form-row input { width: 100%; padding: .72rem 1rem; border: 2px solid var(--border); border-radius: var(--r-sm); font-size: .95rem; color: var(--text); background: var(--cream-bg); transition: border-color .2s, background .2s; } .form-row input:focus { outline: none; border-color: var(--navy); background: white; } .btn-cta { width: 100%; padding: .85rem; background: var(--orange); color: white; border-radius: var(--r-sm); font-family: var(--title-font); font-size: .95rem; font-weight: 700; letter-spacing: .02em; margin-top: .5rem; transition: background .2s, transform .1s; } .btn-cta:hover { background: #c53d00; } .btn-cta:active { transform: scale(.98); } .btn-cta:disabled { background: #ccc; cursor: not-allowed; } #lg-error { display: none; background: var(--orange-lt); border: 1.5px solid var(--orange); color: var(--orange); padding: .65rem 1rem; border-radius: var(--r-sm); font-size: .85rem; margin-top: .75rem; } .lg-return { text-align: center; font-size: .85rem; color: var(--text-mid); margin-top: .75rem; } .lg-return a { color: var(--navy); font-weight: 600; text-decoration: none; cursor: pointer; } .lg-return a:hover { text-decoration: underline; } /* Return user panel */ .return-panel { background: var(--cream-lt); border-radius: var(--r); padding: 1.25rem; border: 2px dashed var(--cream); margin-bottom: 1.25rem; display: none; } .return-panel h3 { font-family: var(--title-font); font-size: .95rem; color: var(--navy); margin-bottom: .3rem; } .return-panel p { font-size: .85rem; color: var(--text-mid); margin-bottom: 1rem; } /* ── App Header ── */ .app-hdr { background: var(--navy); color: white; padding: .75rem 1.25rem; display: flex; align-items: center; justify-content: space-between; position: sticky; top: 0; z-index: 200; box-shadow: 0 2px 8px rgba(0,0,0,.25); } .hdr-brand { font-family: var(--title-font); font-weight: 900; font-size: .88rem; color: var(--yellow); letter-spacing: .02em; } .hdr-user { font-size: .78rem; opacity: .65; text-align: right; max-width: 200px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .hdr-dots { display: flex; gap: 5px; align-items: center; } .hdr-dot { width: 9px; height: 9px; border-radius: 50%; background: rgba(255,255,255,.2); transition: background .3s; } .hdr-dot.done { background: var(--yellow); } .hdr-dot.cur { background: white; } /* ── Day Nav ── */ .day-nav { background: white; border-bottom: 2px solid var(--cream-lt); padding: .65rem 1rem; display: flex; gap: 7px; overflow-x: auto; -webkit-overflow-scrolling: touch; scrollbar-width: none; } .day-nav::-webkit-scrollbar { display: none; } .dnav-btn { flex: 0 0 auto; padding: .45rem .9rem; border-radius: 999px; border: 2px solid var(--border); background: white; font-family: var(--title-font); font-size: .78rem; font-weight: 700; color: var(--text-mid); white-space: nowrap; transition: all .2s; display: flex; align-items: center; gap: 5px; } .dnav-btn:hover:not(.locked) { border-color: var(--navy); color: var(--navy); } .dnav-btn.active { background: var(--navy); border-color: var(--navy); color: white; } .dnav-btn.done { border-color: var(--yellow); color: var(--navy-dk); background: var(--yellow-lt); } .dnav-btn.locked { opacity: .4; cursor: not-allowed; } /* ── Day Content ── */ .day-content { max-width: 740px; margin: 0 auto; padding: 1.5rem 1rem 5rem; } .day-hdr { background: var(--navy); color: white; border-radius: var(--r); padding: 1.5rem 1.5rem 1.5rem; margin-bottom: 1.25rem; position: relative; overflow: hidden; } .day-hdr::after { content: attr(data-day-num); position: absolute; right: 1rem; bottom: -.75rem; font-family: var(--title-font); font-size: 5.5rem; font-weight: 900; opacity: .07; line-height: 1; pointer-events: none; } .day-pill { display: inline-block; background: var(--yellow); color: var(--navy); font-family: var(--title-font); font-size: .68rem; font-weight: 800; text-transform: uppercase; letter-spacing: .1em; padding: 4px 12px; border-radius: 999px; margin-bottom: .7rem; } .day-hdr h2 { font-family: var(--title-font); font-size: clamp(1.25rem, 3.5vw, 1.6rem); font-weight: 800; line-height: 1.2; margin-bottom: .4rem; position: relative; z-index: 1; } .day-hdr .day-sub { font-size: .88rem; opacity: .75; position: relative; z-index: 1; max-width: 85%; } .day-intro { background: white; border-left: 4px solid var(--yellow); border-radius: 0 var(--r-sm) var(--r-sm) 0; padding: 1rem 1.25rem; font-size: .9rem; color: var(--text-mid); margin-bottom: 1.5rem; box-shadow: var(--shadow); line-height: 1.65; } /* ── Expression Cards ── */ .expr-card { background: white; border-radius: var(--r); box-shadow: var(--shadow); margin-bottom: 1.25rem; overflow: hidden; border: 1px solid rgba(36,55,87,.05); } .expr-hdr { background: var(--navy); padding: 1rem 1.25rem; display: flex; align-items: center; gap: .9rem; } .expr-num { width: 34px; height: 34px; background: var(--yellow); color: var(--navy); border-radius: 50%; display: flex; align-items: center; justify-content: center; font-family: var(--title-font); font-size: .85rem; font-weight: 900; flex-shrink: 0; } .expr-title { font-family: var(--title-font); font-size: 1.1rem; font-weight: 800; color: white; letter-spacing: .02em; } .expr-body { padding: 1.25rem 1.25rem .25rem; } .sect-lbl { font-size: .68rem; text-transform: uppercase; letter-spacing: .1em; font-weight: 700; color: var(--text-soft); margin-bottom: .2rem; } .en-val { font-family: var(--title-font); font-size: 1rem; font-weight: 700; color: var(--orange); margin-bottom: 1rem; } .es-box { background: var(--cream-bg); border-radius: var(--r-sm); padding: .75rem 1rem; font-size: .875rem; color: var(--text-mid); margin-bottom: 1rem; line-height: 1.65; } .examples { margin-bottom: 1rem; } .example { display: flex; gap: .5rem; margin-bottom: .5rem; font-size: .875rem; align-items: flex-start; } .example .bullet { color: var(--orange); font-weight: 800; flex-shrink: 0; padding-top: .05rem; } .example .content { line-height: 1.5; } .example .es-line { color: var(--navy); font-weight: 600; display: block; } .example .en-line { color: var(--text-soft); display: block; font-size: .82rem; } .nc-box { background: #fff8ec; border-left: 3px solid var(--yellow); border-radius: 0 var(--r-sm) var(--r-sm) 0; padding: .7rem 1rem; font-size: .85rem; color: var(--text); margin-bottom: 1rem; line-height: 1.55; } .nc-box .nc-lbl { font-size: .65rem; text-transform: uppercase; letter-spacing: .09em; font-weight: 800; color: var(--orange); display: block; margin-bottom: .25rem; } .turno-box { background: #f0f4ff; border: 2px dashed #b8c5e0; border-radius: var(--r-sm); padding: .85rem 1rem; margin-bottom: 1rem; } .turno-box .tt-lbl { font-size: .65rem; text-transform: uppercase; letter-spacing: .09em; font-weight: 800; color: var(--navy); display: block; margin-bottom: .3rem; } .turno-box .tt-q { font-size: .875rem; color: var(--text-mid); margin-bottom: .5rem; } .turno-input { width: 100%; padding: .55rem .8rem; border: 1.5px solid #b8c5e0; border-radius: 6px; font-size: .875rem; background: white; resize: vertical; min-height: 56px; color: var(--text); transition: border-color .2s; } .turno-input:focus { outline: none; border-color: var(--navy); } .turno-saved { font-size: .68rem; color: #3bbd6a; font-weight: 700; margin-top: .3rem; opacity: 0; transition: opacity .3s; letter-spacing: .02em; display: block; text-align: right; } .turno-saved.show { opacity: 1; } /* Mastery tracker */ .mastery-area { padding: 0 1.25rem 1.25rem; } .mastery-lbl { font-size: .68rem; text-transform: uppercase; letter-spacing: .09em; font-weight: 700; color: var(--text-soft); margin-bottom: .45rem; } .mastery-btns { display: flex; gap: 6px; } .m-btn { flex: 1; padding: 10px 6px; border-radius: var(--r-sm); border: 2px solid var(--border); background: white; font-size: .7rem; font-weight: 700; color: var(--text-mid); text-align: center; transition: all .18s; line-height: 1.3; cursor: pointer; font-family: var(--title-font); min-height: 48px; -webkit-tap-highlight-color: transparent; } .m-btn:hover { border-color: var(--navy); color: var(--navy); } .m-btn.lv1.on { background: var(--cream-lt); border-color: var(--cream); color: var(--text); } .m-btn.lv2.on { background: var(--yellow-lt); border-color: var(--yellow); color: var(--navy-dk); } .m-btn.lv3.on { background: var(--navy); border-color: var(--navy); color: white; } /* ── Day Footer ── */ .day-reto { background: var(--navy); color: white; border-radius: var(--r); padding: 1.4rem 1.5rem; margin: 1.5rem 0 1rem; } .day-reto h3 { font-family: var(--title-font); font-size: .8rem; font-weight: 800; color: var(--yellow); text-transform: uppercase; letter-spacing: .08em; margin-bottom: .5rem; } .day-reto p { font-size: .9rem; opacity: .85; line-height: 1.65; margin-bottom: 1rem; } /* ── Mastery Progress Bar ── */ .mastery-progress { margin-bottom: .85rem; } .mp-bar-wrap { height: 6px; background: rgba(255,255,255,.18); border-radius: 99px; overflow: hidden; margin-bottom: .4rem; } .mp-bar { height: 100%; background: var(--yellow); border-radius: 99px; transition: width .35s ease; } .mp-txt { display: block; font-size: .75rem; color: rgba(255,255,255,.65); font-weight: 600; letter-spacing: .01em; text-align: center; } .mp-txt.mp-done { color: #7dffb3; } .btn-complete { width: 100%; padding: .85rem; border-radius: var(--r-sm); font-family: var(--title-font); font-size: 1rem; font-weight: 800; letter-spacing: .02em; transition: all .2s; border: none; cursor: pointer; min-height: 52px; } .btn-complete.pending { background: rgba(255,255,255,.12); color: rgba(255,255,255,.35); cursor: not-allowed; } .btn-complete.pending.ready { background: var(--yellow); color: var(--navy); cursor: pointer; } .btn-complete.pending.ready:hover { background: #ffc96e; } .btn-complete.done-state { background: rgba(255,255,255,.15); color: rgba(255,255,255,.55); cursor: default; } .btn-complete:disabled { opacity: 1; } /* override browser default — we style disabled state ourselves */ .nzd { text-align: center; font-size: .78rem; color: rgba(255,255,255,.5); margin-top: .75rem; } .nzd strong { color: rgba(255,255,255,.8); } /* ── Upsell Banner (Day 7) ── */ .upsell-banner { background: linear-gradient(135deg, var(--orange) 0%, #e85a00 100%); color: white; border-radius: var(--r); padding: 1.75rem 1.5rem; text-align: center; margin-top: 1.5rem; box-shadow: var(--shadow-md); } .upsell-banner h3 { font-family: var(--title-font); font-size: 1.35rem; font-weight: 900; margin-bottom: .5rem; line-height: 1.2; } .upsell-banner p { font-size: .9rem; opacity: .9; margin-bottom: 1.25rem; line-height: 1.6; max-width: 400px; margin-left: auto; margin-right: auto; } .upsell-price-big { font-family: var(--title-font); font-size: 2.75rem; font-weight: 900; color: var(--yellow); line-height: 1; } .upsell-price-sub { font-size: .8rem; opacity: .8; display: block; margin-bottom: 1.25rem; } .btn-upsell { display: inline-block; padding: .85rem 2.25rem; background: white; color: var(--orange); border-radius: var(--r-sm); font-family: var(--title-font); font-size: 1rem; font-weight: 800; text-decoration: none; transition: all .2s; border: none; cursor: pointer; } .btn-upsell:hover { background: var(--yellow); color: var(--navy); } .upsell-includes { margin-top: 1rem; font-size: .78rem; opacity: .7; line-height: 1.8; } /* ── Upsell Modal ── */ .modal-overlay { display: none; position: fixed; inset: 0; background: rgba(0,0,0,.65); z-index: 1000; align-items: center; justify-content: center; padding: 1rem; } .modal-overlay.open { display: flex; } .modal { background: white; border-radius: var(--r); max-width: 460px; width: 100%; overflow: hidden; box-shadow: var(--shadow-lg); animation: popIn .28s ease; } @keyframes popIn { from { opacity: 0; transform: scale(.9) translateY(16px); } to { opacity: 1; transform: scale(1) translateY(0); } } .modal-hdr { background: var(--navy); padding: 1.5rem; text-align: center; } .modal-hdr .modal-emoji { font-size: 2rem; display: block; margin-bottom: .5rem; } .modal-hdr h2 { font-family: var(--title-font); font-size: 1.35rem; font-weight: 900; color: white; margin-bottom: .25rem; } .modal-hdr p { font-size: .875rem; color: rgba(255,255,255,.7); } .modal-body { padding: 1.5rem; } .modal-price { text-align: center; margin-bottom: 1.25rem; } .modal-price .p-main { font-family: var(--title-font); font-size: 2.75rem; font-weight: 900; color: var(--orange); } .modal-price .p-sub { font-size: .85rem; color: var(--text-mid); } .modal-includes { list-style: none; margin-bottom: 1.25rem; } .modal-includes li { display: flex; gap: .6rem; font-size: .88rem; padding: .45rem 0; border-bottom: 1px solid var(--cream-lt); color: var(--text); align-items: flex-start; } .modal-includes li .ck { color: var(--orange); font-weight: 900; font-size: 1rem; flex-shrink: 0; } .btn-book { width: 100%; padding: .95rem; background: var(--orange); color: white; border-radius: var(--r-sm); font-family: var(--title-font); font-size: 1.05rem; font-weight: 800; text-decoration: none; display: block; text-align: center; margin-bottom: .75rem; transition: background .2s; cursor: pointer; border: none; } .btn-book:hover { background: #c53d00; } .btn-modal-close { width: 100%; padding: .65rem; background: none; border: 2px solid var(--border); border-radius: var(--r-sm); font-size: .85rem; color: var(--text-mid); cursor: pointer; transition: border-color .2s, color .2s; } .btn-modal-close:hover { border-color: var(--navy); color: var(--navy); } /* ── Toast ── */ #toast { position: fixed; bottom: 1.5rem; left: 50%; transform: translateX(-50%) translateY(100px); background: var(--navy); color: white; padding: .7rem 1.5rem; border-radius: 999px; font-size: .85rem; font-weight: 600; z-index: 2000; transition: transform .3s ease; white-space: nowrap; max-width: calc(100vw - 2rem); text-align: center; } #toast.show { transform: translateX(-50%) translateY(0); } #toast.success-t { background: #1a6b3c; } #toast.error-t { background: var(--orange); } /* ── Congrats banner (Day 7 complete) ── */ .congrats-banner { background: linear-gradient(135deg, #1a6b3c, #2a8a50); color: white; border-radius: var(--r); padding: 1.5rem; text-align: center; margin-bottom: 1rem; display: none; } .congrats-banner.show { display: block; } .congrats-banner h3 { font-family: var(--title-font); font-size: 1.2rem; font-weight: 800; margin-bottom: .4rem; } .congrats-banner p { font-size: .875rem; opacity: .9; } /* ── Section Tabs ── */ .section-tabs { display: flex; background: white; border-bottom: 2px solid var(--border); position: sticky; top: 0; z-index: 50; } .section-tab { flex: 1; padding: .7rem .5rem; text-align: center; font-family: var(--title-font); font-size: .75rem; font-weight: 700; letter-spacing: .05em; text-transform: uppercase; color: var(--text-soft); border: none; background: none; cursor: pointer; border-bottom: 3px solid transparent; margin-bottom: -2px; transition: color .18s, border-color .18s; } .section-tab.active { color: var(--navy); border-bottom-color: var(--navy); } .section-tab:hover:not(.active) { color: var(--text-mid); } /* ── Materials Section ── */ .mat-section { display: none; padding: 1.25rem 1rem 5rem; } .mat-section.active { display: block; } .mat-intro { font-size: .875rem; color: var(--text-mid); margin-bottom: 1.25rem; line-height: 1.6; } .mat-grid { display: flex; flex-direction: column; gap: .875rem; } .mat-tile { background: white; border-radius: var(--r); overflow: hidden; box-shadow: var(--shadow); display: flex; flex-direction: column; transition: box-shadow .2s; } .mat-tile:hover { box-shadow: var(--shadow-md); } .mat-tile-row { display: flex; min-height: 88px; } .mat-tile-icon { width: 70px; flex-shrink: 0; display: flex; align-items: center; justify-content: center; font-size: 1.7rem; background: var(--cream-lt); border-right: 1px solid var(--border); } .mat-tile.locked .mat-tile-icon { filter: grayscale(1); background: #ececec; } .mat-tile-body { flex: 1; padding: .8rem 1rem; display: flex; flex-direction: column; justify-content: center; } .mat-tile-title { font-family: var(--title-font); font-size: .92rem; font-weight: 700; color: var(--navy); margin-bottom: .15rem; } .mat-tile.locked .mat-tile-title { color: var(--text-mid); } .mat-tile-sub { font-size: .75rem; color: var(--text-soft); margin-bottom: .5rem; line-height: 1.4; } .mat-tile-cta { display: flex; gap: .5rem; flex-wrap: wrap; align-items: center; } .mat-free-btn { display: inline-block; font-size: .76rem; font-family: var(--title-font); font-weight: 700; padding: 5px 14px; border-radius: 999px; background: var(--navy); color: white; border: none; cursor: pointer; transition: background .18s; text-decoration: none; } .mat-free-btn:hover { background: var(--navy-lt); } .mat-lock-badge { display: inline-flex; align-items: center; gap: 4px; font-size: .7rem; font-family: var(--title-font); font-weight: 700; padding: 4px 11px; border-radius: 999px; background: var(--cream-lt); color: var(--text-mid); border: 1px solid var(--border); } .mat-download-btn { display: inline-block; font-size: .76rem; font-family: var(--title-font); font-weight: 700; padding: 5px 14px; border-radius: 999px; background: #1a6b3c; color: white; border: none; cursor: pointer; transition: background .18s; text-decoration: none; } .mat-download-btn:hover { background: #145730; } .mat-buy-strip { display: flex; flex-direction: column; gap: .4rem; background: var(--cream-lt); border-top: 1px solid var(--border); padding: .75rem 1rem; } .mat-buy-primary { display: block; text-align: center; text-decoration: none; background: var(--orange); color: white; border: none; cursor: pointer; border-radius: var(--r-sm); padding: .6rem 1rem; font-family: var(--title-font); font-size: .85rem; font-weight: 800; transition: background .18s; } .mat-buy-primary:hover { background: #c53d00; } .mat-buy-secondary { display: block; text-align: center; text-decoration: none; background: none; color: var(--navy); border: 2px solid var(--navy); cursor: pointer; border-radius: var(--r-sm); padding: .5rem 1rem; font-family: var(--title-font); font-size: .78rem; font-weight: 700; transition: all .18s; } .mat-buy-secondary:hover { background: var(--navy); color: white; } .mat-sessions-label { text-align: center; font-size: .66rem; font-family: var(--title-font); font-weight: 700; color: var(--text-mid); letter-spacing: .05em; text-transform: uppercase; margin: .2rem 0 0; } .mat-session-link { display: block; text-align: center; text-decoration: none; background: none; color: var(--navy); border: 1.5px solid var(--navy); cursor: pointer; border-radius: var(--r-sm); padding: .38rem .75rem; font-family: var(--title-font); font-size: .74rem; font-weight: 700; transition: all .18s; } .mat-session-link:hover { background: var(--navy); color: white; } @media (max-width: 520px) { .day-content { padding: 1rem .75rem 5rem; } .lg-hero { padding: 2rem 1.25rem 2.5rem; } .mastery-btns { gap: 4px; } .m-btn { font-size: .64rem; padding: 6px 4px; } }` ].join('\n'); shadow.appendChild(styleEl); /* ── 3. Inject HTML into shadow ── */ var container = document.createElement('div'); container.innerHTML = `
The Spanish Wey
Tutor Jesús
Tutor Jesús — The Spanish Wey

7 días de español
que SÍ vas a usar

35 expresiones reales. Un hábito diario. Sin memorizar listas aburridas.

35Expresiones
7Días
100%Gratis

Empieza gratis ahora

Tu progreso se guarda — puedes continuar en cualquier momento desde cualquier dispositivo.

¿Ya empezaste? Continúa aquí

Bienvenido de vuelta

Ingresa tu email para retomar donde lo dejaste.

The Spanish Wey
The Spanish Wey
`; shadow.appendChild(container); /* ── 4. Shadow-aware document proxy ── */ /* The app JS uses document.getElementById / querySelector / querySelectorAll / createElement — we route the DOM-lookup calls into the shadow root while letting createElement go to the real document (shadow DOM doesn't restrict that). */ var shadowDoc = { getElementById: function(id) { return shadow.getElementById(id); }, querySelector: function(sel) { return shadow.querySelector(sel); }, querySelectorAll: function(sel) { return shadow.querySelectorAll(sel); }, createElement: function(tag) { return document.createElement(tag); }, addEventListener: function() { return document.addEventListener.apply(document, arguments); }, removeEventListener: function() { return document.removeEventListener.apply(document, arguments); }, /* Safety net: anything else the app might call on document */ get body() { return shadow.querySelector('body') || container; }, get head() { return shadow; }, get documentElement() { return shadow.querySelector('html') || container; }, }; /* ── 5. Load Supabase, then run app ── */ function runApp() { /* Wrap app JS so its 'document' references hit the shadow proxy. window, localStorage, fetch, console etc. are untouched — they resolve from the outer closure as normal globals. */ (function (document) { /* ───────────────────────────────────────── CONFIG ───────────────────────────────────────── */ const SB_URL = 'https://ywgbimvzvaaosorplpnz.supabase.co'; const SB_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl3Z2JpbXZ6dmFhb3NvcnBscG56Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3ODUzNjYzMjksImV4cCI6MjEwMDk0MjMyOX0.vRXdM6BLZWMFaizmgF1gaqcEBKLj6kOwM34mT0W3Fpc'; const N8N_URL = 'https://n8n.boldwright.com/webhook/manychat-jesus-lead-capture'; const BOOKING = 'https://cal.com/tutor-jesus-hhp4fp/30min'; // TODO: Replace with real Stripe Checkout links once wired const STRIPE_PYP = 'https://buy.stripe.com/dRm3cw0jF5DO2qbeJxdMI03'; // Por y Para PDF — $10 one-time const STRIPE_SESSION_ONE = 'https://buy.stripe.com/3cIaEY4zV3vG9SD8l9dMI01'; // One Lesson + digital practica const STRIPE_SESSION_STARTER = 'https://buy.stripe.com/14A8wQfezfeo8Oz8l9dMI02'; // Starter Pack — 4 lessons + digital const STRIPE_SESSION_PREMIUM = 'https://buy.stripe.com/dRmcN6aYjaY89SD44TdMI00'; // Premium — 10 sessions + digital // TODO: PDF download URL once file is in GCS const PDF_PYP_URL = '#'; // e.g. https://storage.googleapis.com/boldwright-media/Jesus-Nolazco/por-y-para.pdf /* ───────────────────────────────────────── EXPRESSION DATA — 7 DAYS × 5 EXPRESSIONS ───────────────────────────────────────── */ const DAYS = [ { num: 1, theme: '🎯', title: 'Lo que quieres y necesitas', sub: '5 expresiones que reemplazan frases enteras en inglés', intro: 'Estas 5 expresiones son pan de cada día para los hispanohablantes. Los nativos las usan constantemente — apréndetelas hoy y mañana ya vas a sonar diferente.', exprs: [ { n:'SE ME ANTOJA', en:'I\'m craving / I feel like having', es:'Lo usas cuando tienes ganas espontáneas de algo — comida, hacer una actividad, ir a algún lugar. No es una necesidad urgente, es un deseo del momento.', exs:[['Se me antoja un taco de canasta ahorita.','I\'m really craving a basket taco right now.'], ['¿A ti se te antoja algo dulce?','Do you feel like something sweet?']], nc:'\'Yo antojo\' NO funciona en español. Siempre es \'se me antoja\' — el pronombre (me/te/le/nos) cambia según quién siente el antojo.', tt:'¿Qué se te antoja en este momento? Puede ser comida, un lugar, una actividad. ¡Dime en español!' }, { n:'ME HACE FALTA', en:'I need / I\'m missing / I could use', es:'Más matizado que \'necesito\'. Dice que algo está ausente y su falta se siente. Puede ser una cosa, una persona, o una habilidad que te gustaría tener.', exs:[['Me hace falta practicar más la pronunciación.','I need to practice my pronunciation more.'], ['Me haces falta. (a una persona) → I miss you / I need you in my life.','']], nc:'\'Me hace falta\' = necesito / extraño. \'Me falta\' = me faltan X unidades (cantidad). \'¿Te falta algo?\' = \'Are you missing anything?\'', tt:'¿Qué te hace falta en tu español ahora mismo? ¿O qué te hace falta en tu vida? Di una frase completa.' }, { n:'ME DA COSA', en:'It makes me feel weird / awkward / uncomfortable', es:'Esa sensación de incomodidad, vergüenza leve, o rareza que no sabes bien cómo explicar. Muy usada en México y España para situaciones sociales incómodas.', exs:[['Me da cosa llamarle así de repente.','It feels weird / awkward to call them out of nowhere.'], ['Me da cosa comer solo en un restaurante.','Eating alone at a restaurant makes me self-conscious.']], nc:'\'Cosa\' aquí no es \'thing\' literal — es completamente idiomática. \'Me da pena\' es más específico (embarrassment). \'Me da cosa\' es más general.', tt:'¿Qué situación te da cosa? Puede ser algo social, algo que evitas... cuéntame en español.' }, { n:'ME CUESTA', en:'I find it difficult / It\'s hard for me', es:'Habla de dificultad o esfuerzo personal. \'Cuesta\' viene del verbo que también significa \'to cost\' — como si algo te costara esfuerzo o energía hacerlo.', exs:[['Me cuesta mucho pronunciar la erre.','I find it really hard to pronounce the rolling r.'], ['Me cuesta decir que no.','I have a hard time saying no.']], nc:'\'Me cuesta\' = me resulta difícil (esfuerzo). \'Me cuesta dinero\' = it costs me money. El contexto determina cuál es. \'Me cuesta creerlo\' = I find it hard to believe.', tt:'¿Qué te cuesta en tu aprendizaje de español? Di la frase completa usando \'me cuesta\'.' }, { n:'VALE LA PENA', en:'It\'s worth it', es:'Cuando algo merece el esfuerzo, el tiempo o el sacrificio que requiere. Una de las frases más usadas y más mal traducidas por estudiantes de español.', exs:[['Es difícil, pero vale la pena.','It\'s hard, but it\'s worth it.'], ['¿Vale la pena aprender español? ¡Claro que sí!','Is it worth learning Spanish? Of course it is!']], nc:'No digas \'es worth it\' mezclando inglés. \'Vale\' aquí viene del verbo \'valer\' (to be worth), no de \'okay\' como en España. \'No vale la pena\' = it\'s not worth it.', tt:'Di en voz alta: ¿qué vale la pena en tu vida ahora mismo? Usa la expresión en una frase completa.' } ], reto: 'Elige UNA de las 5 expresiones y úsala en una conversación real hoy — con alguien, contigo mismo en voz alta, o en un voice memo. ¿Cuál vas a elegir?', }, { num: 2, theme: '⏱️', title: 'Cuando falta, sobra o se te va', sub: 'Tiempo, dinero, memoria — el español tiene una expresión para todo', intro: 'Hoy vas a aprender a hablar de lo que te alcanza o no, lo que te conviene, y esas cosas que se te escapan sin querer. Los nativos usamos estas expresiones todo el tiempo.', exprs: [ { n:'NO ME ALCANZA', en:'I don\'t have enough / It\'s not enough for me', es:'Cuando algo — dinero, tiempo, energía — no es suficiente para lo que necesitas. Es LA expresión cotidiana para decir que no tienes suficiente de algo.', exs:[['No me alcanza para el pasaje del metro.','I don\'t have enough for the metro fare.'], ['No me alcanza el tiempo para todo.','I don\'t have enough time for everything.']], nc:'En conversación nunca digas \'no tengo suficiente\' — suena muy de libro de texto. \'No me alcanza\' es lo que usamos en la vida real.', tt:'¿Para qué no te alcanza el tiempo o el dinero ahorita en tu vida? Cuéntame con esta expresión.' }, { n:'ME SOBRA', en:'I have extra / I have more than I need', es:'El opuesto de \'no me alcanza\'. Tienes de más — puede ser espacio, comida, tiempo, energía, o cualquier recurso.', exs:[['Me sobra comida del domingo, ¿quieres?','I have leftover food from Sunday, do you want some?'], ['Hoy me sobra tiempo — ¿qué hacemos?','I have time to spare today — what should we do?']], nc:'\'Me sobra\' (una cosa) vs \'me sobran\' (varias). \'¿Te sobra dinero?\' vs \'¿Te sobran monedas?\'. El número del sustantivo determina el verbo.', tt:'Dime algo que te sobra ahorita — tiempo, comida, ropa, energía... ¡lo que sea en español!' }, { n:'ME CONVIENE', en:'It\'s better for me / It works for me / It\'s in my best interest', es:'No es solo \'me gusta\' — significa que algo es conveniente, práctico o beneficioso específicamente para ti. Más fuerte que una preferencia.', exs:[['Me conviene salir temprano para evitar el tráfico.','It\'s better for me to leave early to avoid traffic.'], ['¿A ti te conviene el martes para la llamada?','Does Tuesday work for you for the call?']], nc:'No confundas con \'convenient\' en inglés que es más superficial. \'Me conviene\' es más amplio — incluye lo bueno, lo práctico Y lo beneficioso para ti.', tt:'¿Qué te conviene hacer esta semana para mejorar tu español? Una frase completa, por favor.' }, { n:'SE ME OLVIDÓ', en:'I forgot / It slipped my mind', es:'El \'se me\' quita la culpa — no es \'I forgot\' activo, sino \'it escaped from me\'. Indica que fue sin querer. Muy importante en cultura mexicana y española.', exs:[['Se me olvidó completamente el paraguas.','I completely forgot my umbrella (it slipped my mind).'], ['¡Se me olvidó avisarte! Lo siento.','I forgot to let you know! I\'m sorry.']], nc:'\'Olvidé\' (sin se me) existe pero suena más formal y más intencional. \'Se me olvidó\' es lo natural en conversación y suaviza la culpa. Mismo con \'se nos olvidó\' (nos).', tt:'¿Qué se te ha olvidado hacer últimamente? Di en voz alta: \'Se me olvidó...\'' }, { n:'SE ME PASÓ', en:'It slipped by / I missed it / Time got away from me', es:'Similar a \'se me olvidó\' pero más para cuando algo se va sin que te des cuenta — una fecha, un momento, una oportunidad, o simplemente el tiempo.', exs:[['Se me pasó la hora de la cita. Lo siento.','I missed the appointment time. I\'m sorry.'], ['El día se me pasó rapidísimo — ni me di cuenta.','The day flew by / Time got away from me today.']], nc:'\'Se me pasó\' puede ser tiempo que se fue \'se me pasó rápido el día\' o algo que olvidaste \'se me pasó avisarte\'. \'Se me olvidó\' = más específico al olvido de una cosa.', tt:'¿Algo que se te pasó esta semana — una fecha, una tarea, un momento importante? Cuéntame.' } ], reto: 'Ahorita mismo piensa en una cosa que se te olvidó hoy y dila en voz alta con \'se me olvidó\'. Después agrega: \'...y me conviene hacerlo hoy.\' ¿Puedes encadenar las dos?', }, { num: 3, theme: '🤦', title: 'Cuando algo te pasa sin planearlo', sub: 'El \'se me\' accidental — y cómo quitarte la culpa en español', intro: 'Hoy dominamos el famoso \'se me\' accidental — esa construcción que los anglohablantes temen pero los nativos usamos todo el tiempo. Una vez que lo entiendas, no podrás dejar de usarlo.', exprs: [ { n:'SE ME ACABÓ', en:'I ran out of / It ran out on me', es:'Cuando algo se terminó — intencionalmente o no. El \'se me\' indica que te afecta directamente. Puede ser físico (comida, gasolina) o metafórico (paciencia, energía).', exs:[['Se me acabó la batería del teléfono en el peor momento.','My phone battery died on me at the worst moment.'], ['Se me acabó la paciencia. No aguanto más.','I ran out of patience. I can\'t take it anymore.']], nc:'\'Se me acabó\' (yo). \'Se te acabó\' (tú). \'Se le acabó\' (él/ella). \'Se nos acabó\' (nosotros). La estructura es igual, solo cambia el pronombre.', tt:'¿Qué se te acaba más seguido en tu vida? ¿Tiempo? ¿Dinero? ¿Energía? Haz una frase completa.' }, { n:'SE ME CAYÓ', en:'I dropped it / It fell on me', es:'Para cuando algo se va de tus manos o cae — siempre accidentalmente. El \'se me\' indica que fue sin querer y que a ti te pasó. Extremadamente común.', exs:[['¡Ay, se me cayó el teléfono!','Oh no, I dropped my phone!'], ['Se me cayó todo cuando abrí la bolsa.','Everything fell out when I opened my bag.']], nc:'En conversación nunca digas \'yo dejé caer\' — es muy literal y raro. \'Se me cayó\' es siempre lo natural. \'Caerse\' (reflexivo) = to fall down (para personas o cosas).', tt:'¿Cuándo fue la última vez que se te cayó algo? Cuéntame qué fue con la expresión.' }, { n:'SE ME PERDIÓ', en:'I lost it / It got lost on me', es:'Para objetos que ya no encuentras — el \'se me\' quita la culpa intencional. No lo perdiste a propósito, simplemente se perdió.', exs:[['Se me perdió la llave del carro y llegué tardísimo.','I lost my car key and arrived super late.'], ['¡Se me perdió el perro! ¿Lo has visto?','I lost my dog! Have you seen him?']], nc:'\'Perdí\' (activo, intencional) vs \'se me perdió\' (accidental). Puedes \'perder\' un partido de fútbol (es el resultado) pero se te \'pierde\' una llave (sin querer).', tt:'¿Qué se te pierde más seguido? Di la expresión completa con un ejemplo real de tu vida.' }, { n:'SE ME HIZO TARDE', en:'I ended up running late / Time got away from me', es:'No es exactamente \'I was late\' — es más poético: el tiempo se te fue sin darte cuenta y de repente ya era tarde. Muy coloquial en México.', exs:[['Perdón que llegué tarde, se me hizo tarde en el trabajo.','Sorry I\'m late, time got away from me at work.'], ['Se me hizo tarde leyendo y no pude salir a tiempo.','I got caught up reading and couldn\'t leave on time.']], nc:'\'Llegué tarde\' = I arrived late (neutral). \'Se me hizo tarde\' = time slipped away and I ended up late (más natural, explica sin tanto juicio). Úsala para disculparte más suavemente.', tt:'¿Con qué actividad se te suele hacer tarde? ¿En qué te pierdes tanto que se te va el tiempo?' }, { n:'ME SURGIÓ ALGO', en:'Something came up', es:'La frase más útil del mundo para explicar un cambio de planes. \'Surgir\' = to arise, to come up. Suena profesional y natural al mismo tiempo.', exs:[['No voy a poder ir esta noche — me surgió algo.','I won\'t be able to make it tonight — something came up.'], ['Me surgió una oportunidad de trabajo increíble esta semana.','An incredible job opportunity came up for me this week.']], nc:'\'Me surgió algo\' es vago (no dices qué). Si quieres ser más específico: \'me surgió una junta\', \'me surgió un compromiso familiar\'. Ambas versiones son naturales.', tt:'¿Cuándo fue la última vez que te surgió algo inesperado? ¿Fue bueno o malo? Cuéntame en español.' } ], reto: 'Practica estas 3 seguidas sin parar: \'Se me cayó el teléfono, se me olvidó el cargador, y se me acabó la batería.\' ¿Puedes decirlas fluidas? Cronométrate.', }, { num: 4, theme: '⏰', title: 'Acciones en el tiempo', sub: 'Lo que acabas de hacer, lo que estás a punto de hacer, y lo que dejaste', intro: 'Estas 5 expresiones son construcciones verbales — no solo vocabulario. Entiéndelas y podrás hablar de acciones y el tiempo de forma mucho más natural y fluida.', exprs: [ { n:'ACABO DE + INFINITIVO', en:'I just did...', es:'Indica que algo ocurrió hace muy poco — segundos o minutos. Es el \'present perfect express\' del español cotidiano.', exs:[['Acabo de llegar a casa. Dame un minuto.','I just got home. Give me a minute.'], ['¿Qué pasó? — Acabo de leer tu mensaje y me preocupé.','What happened? — I just read your message and got worried.']], nc:'Cambia con el sujeto: acabo / acabas / acaba / acabamos / acaban de + infinitivo. \'Acaba de salir\' = he/she just left. No necesitas \'justo\' — \'acabo de\' ya lo dice todo.', tt:'¿Qué acabas de hacer en los últimos 10 minutos? Di 2 cosas usando \'acabo de\'.' }, { n:'ESTOY A PUNTO DE...', en:'I\'m about to...', es:'Algo va a pasar en segundos o minutos — estás en el umbral de hacer algo. Se usa con estar + el infinitivo del verbo siguiente.', exs:[['Estoy a punto de salir. Dame 2 minutos, por favor.','I\'m about to leave. Give me 2 minutes, please.'], ['La película está a punto de empezar, corre.','The movie is about to start, hurry.']], nc:'No lo uses para eventos lejanos — \'Estoy a punto de graduarme en 3 años\' suena muy raro. \'A punto de\' es para lo inmediato, lo inminente.', tt:'¿Qué estás a punto de hacer después de terminar esta sesión? Di la frase completa.' }, { n:'VOLVER A + INFINITIVO', en:'To do something again', es:'Indica repetición de una acción. Más elegante y natural que decir el verbo + \'otra vez\'. \'Volver a intentar\' = to try again.', exs:[['Voy a volver a intentarlo — no me rindo.','I\'m going to try again — I\'m not giving up.'], ['¡No lo vuelvas a hacer! Te lo digo en serio.','Don\'t do it again! I mean it.']], nc:'\'Volver\' solo = to return. \'Volver a\' + infinitivo = to do something again. \'Vuelvo mañana\' = I\'ll be back tomorrow. \'Vuelvo a intentarlo\' = I\'ll try again. El infinitivo es clave.', tt:'¿Qué quieres volver a hacer que dejaste de hacer? Di: \'Quiero volver a...\'' }, { n:'DEJAR DE + INFINITIVO', en:'To stop doing something', es:'Cuando una acción se interrumpe o abandona — temporal o permanentemente. Perfecta para hablar de hábitos que cambiaste o que quieres cambiar.', exs:[['Dejé de tomar café y me siento mucho mejor.','I stopped drinking coffee and I feel much better.'], ['No dejes de practicar — ya vas muy, muy bien.','Don\'t stop practicing — you\'re doing really, really well.']], nc:'¡No confundas! \'Dejar de\' = to stop doing something. \'Dejar\' solo = to leave, to let, to put down. \'Dejé el trabajo\' = I quit the job. \'Dejé de trabajar allí\' = I stopped working there.', tt:'¿Qué has dejado de hacer recientemente? ¿Lo extrañas o fue la mejor decisión? Cuéntame.' }, { n:'A ÚLTIMA HORA', en:'At the last minute / Last-minute (adj)', es:'Algo que pasa justo antes de un deadline o cuando ya casi no queda tiempo. Puede ser un cambio, una decisión, o una acción de emergencia.', exs:[['Siempre hago todo a última hora — así soy.','I always do everything at the last minute — that\'s just how I am.'], ['Nos avisaron a última hora que la junta fue cancelada.','They told us at the last minute that the meeting was cancelled.']], nc:'\'A última hora\' ≠ \'a la última hora\' (at the last hour — no funciona idiomáticamente). La expresión correcta no lleva artículo: \'a última hora\'. Apréndela así.', tt:'¿Eres de los que hacen todo a última hora? Cuéntame un ejemplo real con esta expresión.' } ], reto: 'Crea una historia mini en voz alta con 3 de las expresiones de hoy. Ej: \'Acabo de llegar a casa. Estoy a punto de cenar. Y dejé de comer azúcar esta semana.\' ¡Tú puedes!', }, { num: 5, theme: '📅', title: 'Tiempo, duración y planes con otros', sub: 'Cuánto tardas, cuánto llevas, y con quién quedaste', intro: 'Hoy el tema es el tiempo — no el reloj, sino la duración y los compromisos. Estas expresiones hacen que tu español suene inmediatamente más adulto y natural.', exprs: [ { n:'ALCANZAR A + INFINITIVO', en:'To manage to / To just barely do something', es:'Cuando logras hacer algo justo a tiempo, o cuando tienes el tiempo o la oportunidad suficiente para hacerlo. Matiz de \'barely made it\'.', exs:[['Alcancé a tomar el metro justo antes de que cerrara.','I managed to catch the metro just before it closed.'], ['¿Alcanzas a terminar eso hoy o lo dejamos para mañana?','Can you manage to finish that today or do we leave it for tomorrow?']], nc:'\'Alcanzar a\' tiene matiz de \'justo a tiempo / barely managed\'. Diferente de \'poder\' (can/was able to). \'Pude\' = I was able to. \'Alcancé a\' = I just barely managed to.', tt:'¿Hay algo que alcanzaste a hacer justo a tiempo esta semana? Cuéntame con la expresión.' }, { n:'TARDAR EN...', en:'To take time to do something', es:'Habla del tiempo que le toma a alguien completar una acción. Muy usado para describir procesos, traslados, y habilidades.', exs:[['Tardo 40 minutos en llegar al trabajo en metro.','It takes me 40 minutes to get to work by metro.'], ['¿Cuánto tardas en prepararte por las mañanas?','How long does it take you to get ready in the mornings?']], nc:'No confundas \'tardo\' con \'tarde\' (afternoon). Son completamente diferentes. \'Tardé\' (pasado) = it took me. \'Tardo\' (presente) = it takes me. La tilde importa muchísimo.', tt:'¿Cuánto tardas en llegar a tu trabajo, escuela, o lugar favorito? Di la frase completa.' }, { n:'LLEVAR + TIEMPO + GERUNDIO', en:'To have been doing something for...', es:'Una de las construcciones más importantes del español. Dice cuánto tiempo llevas haciendo algo que todavía continúas haciendo ahora mismo.', exs:[['Llevo 6 meses estudiando español y ya noto la diferencia.','I\'ve been studying Spanish for 6 months and I already notice the difference.'], ['¿Cuánto tiempo llevas viviendo en esta ciudad?','How long have you been living in this city?']], nc:'¡Esto es presente! Todavía lo estás haciendo. \'Llevaba\' (imperfecto) = I had been doing it (and stopped). \'Llevo\' = I\'ve been doing it (still going). Diferencia crucial.', tt:'¿Cuánto tiempo llevas estudiando español? ¡Y luego dime qué más llevas haciendo en tu vida!' }, { n:'QUEDAR CON ALGUIEN', en:'To arrange to meet / To make plans with someone', es:'Cuando coordinas un encuentro con alguien. No es solo \'to meet\' — es el proceso de ACORDAR verse. Muy común en todo el mundo hispanohablante.', exs:[['Quedé con Ana para tomar un café mañana a las 4.','I made plans with Ana to grab coffee tomorrow at 4.'], ['¿Quieres quedar el sábado? Podemos ir al mercado.','Do you want to meet up on Saturday? We could go to the market.']], nc:'\'Quedar con\' = to arrange to meet. \'Conocer a alguien\' = to meet someone for the first time. \'Encontrarse con alguien\' = to run into someone (often unplanned). Tres verbos distintos.', tt:'¿Con quién quieres quedar esta semana y para qué? Di la frase completa en español.' }, { n:'QUEDAR EN ALGO', en:'To agree on something / To decide on something together', es:'Cuando dos o más personas llegan a un acuerdo sobre qué van a hacer o cuándo. Es el resultado — ya decidieron los detalles.', exs:[['Quedamos en vernos el viernes a las 8 en el café de siempre.','We agreed to meet on Friday at 8 at the usual café.'], ['¿En qué quedamos al final? Dime qué decidieron.','What did we end up deciding? Tell me what you all decided.']], nc:'\'Quedar en\' (acordar algo) vs \'quedar con\' (acordar verse con alguien). Misma raíz \'quedar\', preposición diferente, significado diferente. Las dos son esenciales.', tt:'¿En qué has quedado con alguien recientemente? ¿Cumpliste el plan o se te pasó?' } ], reto: 'Habla sobre tu semana usando 3 de estas expresiones: ¿Con quién quedaste? ¿Cuánto tardas en hacer algo? ¿Qué llevas tiempo haciendo? 3 oraciones en voz alta.', }, { num: 6, theme: '💡', title: 'Entender, descubrir y reaccionar', sub: 'Cuando algo hace clic, algo te choca, o de repente todo tiene sentido', intro: 'Las expresiones de hoy son las que usan los nativos cuando procesan información, reaccionan a noticias, y comparten puntos de vista. Son perfectas para conversaciones reales y van a hacer tu español sonar muy auténtico.', exprs: [ { n:'ME DI CUENTA DE QUE...', en:'I realized... / I noticed...', es:'El momento en que algo se vuelve claro para ti — una idea, un patrón, algo que antes no habías notado. Puede ser un insight pequeño o uno que cambia todo.', exs:[['Me di cuenta de que llevo hablando español sin pensar en inglés.','I realized I\'ve been speaking Spanish without thinking in English.'], ['No me di cuenta de que ya era tan tarde — ¡perdón!','I didn\'t realize it was already so late — sorry!']], nc:'Siempre es \'me di cuenta DE que\' — la preposición \'de\' es obligatoria antes de \'que\'. \'Me di cuenta que\' (sin de) es uno de los errores más comunes. No te lo olvides.', tt:'¿De qué te has dado cuenta sobre ti mismo o tu español últimamente? Di la frase completa con \'de que\'.' }, { n:'ME ENTERÉ DE QUE...', en:'I found out that... / I heard that...', es:'Cuando recibes información de algo que pasó — una noticia, un rumor, un hecho nuevo para ti. Siempre viene de una fuente externa, no lo descubriste tú solo.', exs:[['Me enteré de que van a abrir un restaurante nuevo en mi barrio.','I heard they\'re opening a new restaurant in my neighborhood.'], ['¿Ya te enteraste de lo que pasó ayer en el trabajo?','Did you already hear about what happened at work yesterday?']], nc:'\'Me di cuenta\' = lo inferí o noté yo solo (interno). \'Me enteré\' = lo supe por alguien o algo externo. La fuente del conocimiento es lo que los diferencia.', tt:'¿De qué te enteraste recientemente — una noticia, algo de un amigo, algo del trabajo? Cuéntame.' }, { n:'CON RAZÓN', en:'No wonder / That\'s why / Now it makes sense', es:'Cuando entiendes el porqué de algo que antes no tenía lógica. Es una reacción de \'¡Ah, ya entendí!\' — muy natural en conversación.', exs:[['¿No dormiste bien? Con razón estás tan cansado hoy.','You didn\'t sleep well? No wonder you\'re so tired today.'], ['Con razón no contestaba — tenía el teléfono apagado.','No wonder they weren\'t answering — their phone was off.']], nc:'\'Con razón\' = no wonder / that\'s why. \'Tienes razón\' = you\'re right. \'Sin razón\' = for no reason. Mismo núcleo \'razón\' (reason) pero usos totalmente distintos.', tt:'¿Hay algo en tu vida que ahora tiene más sentido que antes? Dilo usando \'con razón...\'.' }, { n:'NO ME CUADRA', en:'It doesn\'t add up / Something\'s off / It doesn\'t make sense', es:'Cuando algo no tiene lógica, no encaja, o algo huele raro. \'Cuadrar\' = to fit / to add up. La usas cuando algo no encaja en tu cabeza.', exs:[['No me cuadra esa historia — hay algo que no me dice.','That story doesn\'t add up — something\'s off.'], ['¿A ti te cuadra lo que dijo en la reunión?','Does what they said in the meeting make sense to you?']], nc:'\'Cuadrar\' normalmente = to fit (como un cuadrado). Aquí es idiomático = to add up / make sense. \'No me cuadra\' tiene un matiz de sospecha. \'No tiene sentido\' es más neutral.', tt:'¿Hay algo en tu vida o en las noticias que no te cuadra últimamente? Exprésalo en español.' }, { n:'TIENE SENTIDO', en:'It makes sense', es:'Simple pero súper versátil. Para confirmar que entiendes algo, que algo es lógico, o para validar lo que alguien explicó. También como pregunta: \'¿tiene sentido?\'', exs:[['Tiene sentido que practiques todos los días si quieres mejorar rápido.','It makes sense that you practice every day if you want to improve quickly.'], ['Ya entendí cómo funciona... tiene sentido, gracias.','I get it now... it makes sense, thanks.']], nc:'\'¿Tiene sentido?\' al final de explicar algo = \'does that make sense? / are you following me?\' Muy útil cuando explicas algo complejo. Más suave que \'¿me entiendes?\'.', tt:'Explícame algo de tu trabajo o tu vida cotidiana y termina con \'...y tiene sentido porque...\'' } ], reto: 'Cuéntame algo que aprendiste esta semana. Usa por lo menos DOS expresiones de hoy en tu historia. ¿Qué entendiste? ¿De qué te enteraste? ¿Qué tiene sentido ahora que antes no tenía?', }, { num: 7, theme: '🎤', title: 'Opiniones sin sonar de libro', sub: '¡Día final! — Habla como tú, no como el textbook', intro: '¡Llegaste al Día 7! Las expresiones de hoy son las que más usamos en conversación para dar opiniones, reaccionar, y manejar la incertidumbre — sin sonar formal. Estas son las que hacen que la gente diga "guau, hablas muy natural."', exprs: [ { n:'ME DA IGUAL', en:'I don\'t mind / Either way / It\'s all the same to me', es:'Cuando dos opciones te parecen igual y no tienes preferencia fuerte. No es indiferencia negativa — es neutralidad genuina. Muy usado en México y España.', exs:[['¿Vamos al cine o al parque? — Me da igual, tú decides.','Should we go to the movies or the park? — Either works for me, you decide.'], ['¿Te molesta si abro la ventana? — No, me da igual.','Do you mind if I open the window? — No, I don\'t mind.']], nc:'No confundas con \'no me importa\' que puede sonar más frío o desinteresado. \'Me da igual\' es neutral y no ofensivo. En México también decimos \'es lo mismo\' para lo mismo.', tt:'¿Hay una decisión donde te da igual? ¿O hay algo donde definitivamente NO te da igual? Cuéntame.' }, { n:'NO PASA NADA', en:'It\'s okay / No worries / Don\'t worry about it', es:'Para tranquilizar a alguien que se disculpó o está preocupado. Es el \'no hay problema\' más cálido y directo. También lo usas cuando tú mismo cometiste un error para quitarle peso.', exs:[['Lo siento, llegué tarde. — No pasa nada, ya te esperaba sentado.','I\'m sorry I\'m late. — No worries, I was already sitting waiting for you.'], ['Se me cayó tu vaso... — No pasa nada, no te preocupes para nada.','I dropped your glass... — It\'s okay, don\'t worry about it at all.']], nc:'\'No pasa nada\' ≠ \'nothing is happening\' (literal). Es puramente idiomatic: \'it\'s all good / no worries\'. \'¿Qué pasa?\' = \'what\'s up / what\'s going on?\' — mismo verbo, diferente uso.', tt:'¿Cuándo fue la última vez que alguien te dijo \'no pasa nada\' o tú lo quisiste decir? La situación.' }, { n:'ME PARECE BIEN / MAL', en:'Sounds good/bad to me / I think it\'s fine/wrong', es:'Para dar tu opinión de forma suave y personal. \'Me parece\' = it seems to me / I think. Más diplomático y menos confrontacional que decir \'¡eso está mal!\'', exs:[['¿Qué te parece si nos vemos el jueves? — Me parece bien.','What do you think about meeting on Thursday? — Sounds good to me.'], ['Me parece mal que no te avisaron con más tiempo.','I think it\'s wrong that they didn\'t give you more notice.']], nc:'\'Me parece\' + adjetivo (bien, mal, raro, interesante, increíble). \'Me parece que\' + oración completa (I think that...). Dos estructuras distintas, ambas muy naturales.', tt:'Da tu opinión sobre algo usando \'me parece\'. Tu trabajo, un lugar, una película, una situación — lo que quieras.' }, { n:'DEPENDE DE...', en:'It depends on...', es:'Para cuando la respuesta no es blanco o negro. \'Depende\' solo también funciona, pero \'depende de\' es más preciso y te lleva naturalmente a explicar de qué depende.', exs:[['¿Cuánto se tarda en aprender español? — Depende de cuánto practiques cada día.','How long does it take to learn Spanish? — It depends on how much you practice each day.'], ['Depende del día — a veces me siento muy bien y a veces no.','It depends on the day — sometimes I feel great and sometimes not.']], nc:'\'Depende\' solo (it depends) ó \'depende de\' + algo específico. Ambas formas son correctas. La segunda te obliga a decir de qué, lo que hace tu respuesta más completa y útil.', tt:'¿Cuándo usas más el español? ¿Depende de algo en tu vida? Cuéntame con la expresión.' }, { n:'DAR POR HECHO', en:'To assume / To take for granted', es:'Dos usos: (1) Asumir sin confirmar — dar algo por cierto sin verificarlo. (2) No apreciar algo que ya tienes porque lo das por sentado.', exs:[['No des por hecho que ya saben la información — confírmalo.','Don\'t assume they already know the information — confirm it.'], ['A veces damos por hecho lo que tenemos y no lo valoramos.','Sometimes we take for granted what we have and don\'t appreciate it.']], nc:'(1) asumir sin confirmar: \'lo di por hecho y estaba equivocado\'. (2) no apreciar: \'no lo des por hecho, es un privilegio\'. El contexto dice cuál es cuál.', tt:'¿Hay algo que has dado por hecho recientemente — en español o en tu vida? Sé honesto contigo mismo.' } ], reto: '¡El reto final! Habla durante 1 minuto sobre cualquier tema que quieras — pero usa al menos 3 de las expresiones de hoy. Grábate si puedes. ¡Demuéstrate lo que ya sabes!', } ]; /* ───────────────────────────────────────── STATE ───────────────────────────────────────── */ let sb, learner, currentDay = 1; let saveTimer = null; /* ───────────────────────────────────────── HELPERS ───────────────────────────────────────── */ function $(id) { return document.getElementById(id); } function show(id) { $(id).classList.add('active'); } function hide(id) { $(id).classList.remove('active'); } let toastTimer; function toast(msg, type = '') { const el = $('toast'); el.textContent = msg; el.className = 'toast show' + (type ? ' ' + type + '-t' : ''); clearTimeout(toastTimer); toastTimer = setTimeout(() => el.classList.remove('show'), 3200); } function setLoading(show) { $('s-loading').style.display = show ? 'flex' : 'none'; } /* ───────────────────────────────────────── INIT ───────────────────────────────────────── */ (async () => { sb = window.supabase.createClient(SB_URL, SB_KEY); // Booking link $('btn-book-modal').href = BOOKING; // Wire login UI $('btn-start').addEventListener('click', handleRegister); $('btn-return').addEventListener('click', handleReturn); $('link-return').addEventListener('click', () => { $('reg-panel').style.display = 'none'; $('ret-panel').style.display = 'block'; }); $('link-newuser').addEventListener('click', () => { $('ret-panel').style.display = 'none'; $('reg-panel').style.display = 'block'; }); $('btn-close-modal').addEventListener('click', () => { $('modal-upsell').classList.remove('open'); }); // Enter key on inputs ['reg-email', 'reg-name', 'reg-ig'].forEach(id => { $(id) && $(id).addEventListener('keydown', e => { if (e.key === 'Enter') handleRegister(); }); }); $('ret-email').addEventListener('keydown', e => { if (e.key === 'Enter') handleReturn(); }); // Try auto-login from localStorage const token = localStorage.getItem('tsw_token'); if (token) { try { const { data } = await sb.rpc('tsw_get_learner', { p_token: token }); if (data) { initApp(data); return; } } catch(e) { /* fall through to login */ } } setLoading(false); show('s-login'); $('reg-name').focus(); })(); /* ───────────────────────────────────────── REGISTER — new learner ───────────────────────────────────────── */ async function handleRegister() { const name = $('reg-name').value.trim(); const email = $('reg-email').value.trim().toLowerCase(); const ig = $('reg-ig').value.trim().replace(/^@/, ''); const errEl = $('lg-error'); errEl.style.display = 'none'; if (!name) { showErr('¿Cómo te llamas? Pon tu nombre para empezar.'); return; } if (!email || !email.includes('@')) { showErr('Necesito tu email para guardar tu progreso.'); return; } $('btn-start').disabled = true; $('btn-start').textContent = 'Un momento...'; try { // Check if email already exists const { data: existing } = await sb.rpc('tsw_get_learner_by_email', { p_email: email }); if (existing) { // Restore session for existing learner localStorage.setItem('tsw_token', existing.learner_token); initApp(existing); return; } // Create new learner const { data: newLearner, error } = await sb.from('tsw_practica_learners') .insert({ email, first_name: name, instagram_handle: ig || null }) .select('*').single(); if (error) throw error; // Save token localStorage.setItem('tsw_token', newLearner.learner_token); // Fire n8n webhook (non-blocking) fireWebhook({ email, first_name: name, instagram_handle: ig || '', keyword: 'practica' }); initApp(newLearner); } catch(e) { showErr('Algo salió mal. Intenta de nuevo en un momento.'); console.error(e); $('btn-start').disabled = false; $('btn-start').textContent = 'Empezar los 7 días →'; } } /* ───────────────────────────────────────── RETURN — existing learner by email ───────────────────────────────────────── */ async function handleReturn() { const email = $('ret-email').value.trim().toLowerCase(); const errEl = $('ret-error'); errEl.style.display = 'none'; if (!email || !email.includes('@')) { errEl.textContent = 'Ingresa un email válido.'; errEl.style.display = 'block'; return; } $('btn-return').disabled = true; $('btn-return').textContent = 'Buscando...'; try { const { data } = await sb.rpc('tsw_get_learner_by_email', { p_email: email }); if (!data) { errEl.textContent = 'No encontré ese email. ¿Quizás te registraste con otro?'; errEl.style.display = 'block'; $('btn-return').disabled = false; $('btn-return').textContent = 'Continuar mi progreso →'; return; } localStorage.setItem('tsw_token', data.learner_token); initApp(data); } catch(e) { errEl.textContent = 'Error de conexión. Intenta de nuevo.'; errEl.style.display = 'block'; $('btn-return').disabled = false; $('btn-return').textContent = 'Continuar mi progreso →'; } } function showErr(msg) { const el = $('lg-error'); el.textContent = msg; el.style.display = 'block'; $('btn-start').disabled = false; $('btn-start').textContent = 'Empezar los 7 días →'; } /* ───────────────────────────────────────── N8N WEBHOOK ───────────────────────────────────────── */ async function fireWebhook(payload) { try { await fetch(N8N_URL, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email: payload.email, first_name: payload.first_name, keyword: payload.keyword || 'practica', platform: 'web', instagram_handle: payload.instagram_handle || '' }) }); } catch(e) { console.warn('Webhook failed (non-critical):', e); } } /* ───────────────────────────────────────── INIT APP ───────────────────────────────────────── */ function initApp(data) { learner = data; currentDay = Math.max(1, Math.min(7, data.day_current || 1)); setLoading(false); hide('s-login'); show('s-app'); // Update last_active sb.from('tsw_practica_learners') .update({ last_active_at: new Date().toISOString() }) .eq('id', learner.id).then(() => {}); buildHeader(); buildDayNav(); renderDay(currentDay); // Wire section tabs document.querySelectorAll('.section-tab').forEach(tab => { tab.addEventListener('click', () => { document.querySelectorAll('.section-tab').forEach(t => t.classList.remove('active')); tab.classList.add('active'); const isPractica = tab.dataset.tab === 'practica'; $('day-nav').style.display = isPractica ? '' : 'none'; $('day-content').style.display = isPractica ? '' : 'none'; if (!isPractica) { buildMaterialsSection(); $('materials-content').classList.add('active'); } else { $('materials-content').classList.remove('active'); } }); }); } /* ───────────────────────────────────────── HEADER ───────────────────────────────────────── */ function buildHeader() { $('hdr-user').textContent = learner.first_name || learner.email.split('@')[0]; updateHeaderDots(); } function updateHeaderDots() { const completed = learner.days_completed || []; const dots = $('hdr-dots'); dots.innerHTML = ''; for (let d = 1; d <= 7; d++) { const dot = document.createElement('div'); dot.className = 'hdr-dot' + (completed.includes(d) ? ' done' : d === currentDay ? ' cur' : ''); dot.title = `Día ${d}`; dots.appendChild(dot); } } /* ───────────────────────────────────────── DAY NAVIGATION ───────────────────────────────────────── */ function buildDayNav() { const nav = $('day-nav'); nav.innerHTML = ''; const completed = learner.days_completed || []; for (let d = 1; d <= 7; d++) { const unlocked = isUnlocked(d); const isDone = completed.includes(d); const isActive = d === currentDay; const btn = document.createElement('button'); btn.className = 'dnav-btn' + (isActive ? ' active' : '') + (isDone && !isActive ? ' done' : '') + (!unlocked ? ' locked' : ''); btn.dataset.day = d; btn.textContent = `Día ${d}`; if (isDone && !isActive) btn.textContent = '✓ Día ' + d; if (!unlocked) btn.textContent = `Día ${d} —`; if (unlocked) { btn.addEventListener('click', () => { currentDay = d; buildDayNav(); updateHeaderDots(); renderDay(d); window.scrollTo({ top: 0, behavior: 'smooth' }); }); } nav.appendChild(btn); } } function isUnlocked(day) { if (day === 1) return true; const completed = learner.days_completed || []; return completed.includes(day - 1); } /* ───────────────────────────────────────── RENDER DAY ───────────────────────────────────────── */ function renderDay(dayNum) { const day = DAYS[dayNum - 1]; const completed = (learner.days_completed || []).includes(dayNum); const mastery = (learner.mastery || {}); const dayKey = 'd' + dayNum; const dayMastery = mastery[dayKey] || {}; const tuTurno = (learner.tu_turno || {}); let html = `
Día ${day.num} de 7

${day.title}

${day.sub}

${day.intro}
`; // Expression cards day.exprs.forEach((expr, i) => { const exIdx = i + 1; const eKey = 'e' + exIdx; const lvl = parseInt(dayMastery[eKey] || 0); const savedText = tuTurno[dayKey + eKey] || ''; html += `
${exIdx}
${expr.n}
Closest English
${expr.en}
En Español
${expr.es}
Ejemplos
${expr.exs.map(([es, en]) => `
» ${es} ${en ? `${en}` : ''}
`).join('')}
No confundas ${expr.nc}
Tu turno — habla

${expr.tt}

✓ guardado
¿Cómo te fue con esta expresión?
`; }); // Day footer / reto const isDone = (learner.days_completed || []).includes(dayNum); const totalExprs = day.exprs.length; const ratedCount = Object.keys(dayMastery).length; const allRated = ratedCount >= totalExprs; const btnDisabled = isDone || (!allRated); const btnClass = isDone ? 'done-state' : (allRated ? 'pending ready' : 'pending'); const btnLabel = isDone ? '✓ Día ' + dayNum + ' completado' : '✓ Marqué el reto del día como hecho'; html += `

Reto del día ${dayNum}

${day.reto}

${!isDone ? `
${ratedCount === totalExprs ? '¡Listo! Ya puedes completar el día ✓' : ratedCount + ' de ' + totalExprs + ' expresiones valoradas' }
` : ''}
No Zero Day: cualquier cosa cuenta. Una expresión. Un minuto. No te rajes.
`; // Day 7 congrats + upsell banner if (dayNum === 7 && isDone) { html += `

¡Completaste los 7 días!

35 expresiones reales. Eso es un logro serio. Ahora imagínate con un plan completo de 90 días.

`; html += buildUpsellBanner(); } else if (dayNum === 7 && !isDone) { html += `
Completa el reto del día 7 para ver tu siguiente paso.
`; } $('day-content').innerHTML = html; // Wire mastery buttons document.querySelectorAll('.m-btn').forEach(btn => { btn.addEventListener('click', handleMastery); }); // Wire tu turno textarea autosave document.querySelectorAll('.turno-input').forEach(ta => { ta.addEventListener('input', handleTuTurno); }); // Wire complete button const completeBtn = $('btn-complete-day'); if (completeBtn && !isDone) { completeBtn.addEventListener('click', handleCompleteDay); } } /* ───────────────────────────────────────── MASTERY BUTTONS ───────────────────────────────────────── */ function handleMastery(e) { const btn = e.currentTarget; const day = btn.dataset.day; const expr = btn.dataset.expr; const lv = parseInt(btn.dataset.lv); const dayKey = 'd' + day; const exKey = 'e' + expr; // Update UI const row = btn.closest('.mastery-btns'); row.querySelectorAll('.m-btn').forEach(b => b.classList.remove('on')); btn.classList.add('on'); // Update local state if (!learner.mastery) learner.mastery = {}; if (!learner.mastery[dayKey]) learner.mastery[dayKey] = {}; learner.mastery[dayKey][exKey] = lv; // Update completion gate checkDayCompletable(parseInt(day)); // Debounced save clearTimeout(saveTimer); saveTimer = setTimeout(saveProgress, 1500); } /* ───────────────────────────────────────── COMPLETION GATE ───────────────────────────────────────── */ function checkDayCompletable(dayNum) { const btn = $('btn-complete-day'); if (!btn || btn.classList.contains('done-state')) return; const day = DAYS[dayNum - 1]; const totalExprs = day.exprs.length; const dayKey = 'd' + dayNum; const dayMastery = (learner.mastery || {})[dayKey] || {}; const ratedCount = Object.keys(dayMastery).length; const allRated = ratedCount >= totalExprs; // Update progress bar const bar = document.querySelector('.mp-bar'); const txt = $('mp-txt'); if (bar) bar.style.width = Math.round(ratedCount / totalExprs * 100) + '%'; if (txt) { txt.textContent = allRated ? '¡Listo! Ya puedes completar el día ✓' : ratedCount + ' de ' + totalExprs + ' expresiones valoradas'; if (allRated) txt.classList.add('mp-done'); } // Enable/disable button btn.disabled = !allRated; if (allRated) { btn.classList.remove('pending'); btn.classList.add('pending', 'ready'); } } /* ───────────────────────────────────────── TU TURNO AUTOSAVE ───────────────────────────────────────── */ function handleTuTurno(e) { const ta = e.currentTarget; const day = ta.dataset.day; const expr = ta.dataset.expr; const key = 'd' + day + 'e' + expr; const indId = 'ts-d' + day + 'e' + expr; if (!learner.tu_turno) learner.tu_turno = {}; learner.tu_turno[key] = ta.value; // Hide indicator while typing const ind = $(indId); if (ind) ind.classList.remove('show'); clearTimeout(saveTimer); saveTimer = setTimeout(async () => { await saveProgress(); // Show ✓ guardado briefly if (ind) { ind.classList.add('show'); setTimeout(() => ind.classList.remove('show'), 2500); } }, 2000); } /* ───────────────────────────────────────── COMPLETE DAY ───────────────────────────────────────── */ async function handleCompleteDay(e) { const dayNum = parseInt(e.currentTarget.dataset.day); const btn = $('btn-complete-day'); btn.disabled = true; btn.textContent = 'Guardando...'; // Update local state if (!learner.days_completed) learner.days_completed = []; if (!learner.days_completed.includes(dayNum)) { learner.days_completed.push(dayNum); } const nextDay = Math.min(7, dayNum + 1); learner.day_current = nextDay; try { await sb.rpc('tsw_complete_day', { p_token: learner.learner_token, p_days_completed: learner.days_completed, p_day_current: learner.day_current, p_mastery: learner.mastery || {}, p_tu_turno: learner.tu_turno || {}, p_mark_upsell: dayNum === 7 }); toast('¡Día ' + dayNum + ' completado!', 'success'); // Update nav + header currentDay = dayNum; buildDayNav(); updateHeaderDots(); renderDay(dayNum); // Day 7 — flag as hot lead in HubSpot, show upsell modal if (dayNum === 7) { // Fire practica_complete webhook → n8n sets hs_lead_status: OPEN in HubSpot fireWebhook({ email: learner.email, first_name: learner.first_name || '', instagram_handle: learner.instagram_handle || '', keyword: 'practica_complete' }); setTimeout(() => { $('modal-upsell').classList.add('open'); }, 1200); } } catch(err) { console.error(err); toast('Error al guardar. Intenta de nuevo.', 'error'); btn.disabled = false; btn.textContent = '✓ Marqué el reto del día como hecho'; } } /* ───────────────────────────────────────── SAVE PROGRESS (debounced) ───────────────────────────────────────── */ async function saveProgress() { if (!learner) return; try { await sb.rpc('tsw_save_progress', { p_token: learner.learner_token, p_mastery: learner.mastery || {}, p_tu_turno: learner.tu_turno || {} }); } catch(e) { console.warn('Auto-save failed:', e); } } /* ───────────────────────────────────────── MATERIALS SECTION Unlock logic: learner.tier !== 'free' unlocks all items. Per-item purchase (e.g. just the PDF) requires a purchased_items jsonb column + Stripe webhook — add that once STRIPE_PYP checkout is live. ───────────────────────────────────────── */ const MATERIALS = [ { id: 'practica7', icon: '📅', title: '7-Day Practica', sub: 'Tu curso gratuito — 35 expresiones reales con nativos', free: true, }, { id: 'por_y_para', icon: '📄', title: 'Por y Para', sub: 'Guía PDF — domina la diferencia de una vez por todas', free: false, price: '$10', }, ]; function buildMaterialsSection() { const el = $('materials-content'); const hasAccess = learner && learner.tier && learner.tier !== 'free'; let html = '

Material de The Spanish Wey — lo que Jesús usa con sus estudiantes.

'; MATERIALS.forEach(item => { const unlocked = item.free || hasAccess; if (item.free) { html += `
${item.icon}
${item.title}
${item.sub}
`; } else if (unlocked) { html += `
${item.icon}
${item.title}
${item.sub}
`; } else { html += `
${item.icon}
${item.title}
${item.sub}
🔒 ${item.price}
`; } }); html += '
'; el.innerHTML = html; // Wire "go to Practica" button (can't use onclick in innerHTML — shadow DOM) const btn = el.querySelector('.go-practica-btn'); if (btn) { btn.addEventListener('click', () => { $('tab-practica').click(); }); } } /* ───────────────────────────────────────── UPSELL BANNER (Day 7) ───────────────────────────────────────── */ function buildUpsellBanner() { return `

¿Listo para el siguiente nivel?

Esto fue solo el comienzo. El plan de 90 días con Tutor Jesús te da 50+ expresiones más, 10 sesiones 1:1, y seguimiento por WhatsApp.

$300
pago único · 10 sesiones · material completo de por vida
✓ 10 sesiones 1:1  ·  ✓ Guía de 50+ expresiones  ·  ✓ Check-ins WhatsApp  ·  ✓ Plan personalizado
`; } })(shadowDoc); } if (window.supabase) { runApp(); } else { var s = document.createElement('script'); s.src = 'https://cdn.jsdelivr.net/npm/@supabase/supabase-js@2/dist/umd/supabase.js'; s.onload = runApp; document.head.appendChild(s); } })();