1<!DOCTYPE html> 2<html lang="en"> 3<head> 4 <meta charset="UTF-8"> 5 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 6 <title>Vajo.com - Login</title> 7
7<script src="https://cdn.tailwindcss.com"></script>
vendor: 85 bytes, lines 7-9
7 8 <script src="https://www.google.com/recaptcha/api.js" async defer></script> 9
9<style> 10 @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;600;800&display=swap'); 11 body { font-family: 'Inter', sans-serif; background-color: #f8fafc; } 12 .g-recaptcha > div { margin: 0 auto; } /* Centers the widget */ 13 </style> 14<!-- Google Tag Manager -->
15<script>
vendor: 327 bytes, lines 15-19
15(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start': 16new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0], 17j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src= 18'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f); 19})(window,document,'script','dataLayer','
19GTM-5TCHGBTM
vendor: 3 bytes, line 19
19');
19</script>
19 20<!-- End Google Tag Manager --> 21<!-- Privacy-friendly analytics by Plausible -->
22<script async src="https://plausible.io/js/pa-r-Vzsm9Uo63uO6vR2faMY.js"></script>
vendor: 1 bytes, line 22
22
23<script> 24 window.plausible=window.plausible||function(){(plausible.q=plausible.q||[]).push(arguments)},plausible.init=plausible.init||function(i){plausible.o=i||{}}; 25 plausible.init() 26</script>
26 27<!-- Tolt affiliate/referral tracking â sets window.tolt_referral once a 28 visitor arrives via an affiliate's link. See includes/affiliates.php's 29 docblock for how this fits alongside (not instead of) our own ?ref= 30 cookie system. -->
31<script async src="https://files.tlt-cdn.com/tlt.js" data-tolt="pk_fVJDNvT4Dn4QBooB93acc4Po"></script>
31 32<!-- User-journey events: custom funnel events (Plausible + GTM dataLayer) + a data-evt tagging hook -->
33<script> 34(function () { 35 function track(name, props) { 36 if (!name) return; 37 // Plausible custom event 38 if (typeof window.plausible === 'function') { 39 window.plausible(name, props ? { props: props } : undefined); 40 } 41 // GTM dataLayer (only present when GTM is enabled) â usable by GA4 42 if (window.dataLayer && typeof window.dataLayer.push === 'function') { 43 var payload = { event: name }; 44 if (props) { for (var k in props) { if (Object.prototype.hasOwnProperty.call(props, k)) payload[k] = props[k]; } } 45 window.dataLayer.push(payload); 46 } 47 } 48 // Exposed globally so other inline scripts on the page (e.g. 49 // includes/signup-gate.php's popup) can fire the same funnel events 50 // without duplicating the Plausible/GTM dispatch logic. 51 window.lmTrack = track; 52 53 // Appends ": variant" to an event name on the 3 shuffled landing pages 54 // (index / learn-german / speak-german) â same "Landing View: index" / 55 // "Sign Up: index" convention already used above and in 56 // includes/signup-gate.php â so a single call site works both there and on 57 // pages that don't set LM_LANDING_VARIANT (membership/index.php, 58 // sales2.php, â¦), where the name is used as-is. 59 window.lmTrackVariant = function (base) { 60 return window.LM_LANDING_VARIANT ? (base + ': ' + window.LM_LANDING_VARIANT) : base; 61 }; 62 63 // Checkout events carry TWO dimensions (which tier + which page), so the 64 // tier goes after the first colon (matching the convention above) and the 65 // page goes in parens after that, e.g. "Checkout Click: PRO (index)" â vs. 66 // just "Checkout Click: PRO" on pages with no landing variant (sales2.php, 67 // membership/index.php). Keeps every checkout drop-off point self- 68 // explanatory about exactly which plan AND which page it happened on. 69 window.lmTrackCheckout = function (stage, tierLabel) { 70 var base = 'Checkout ' + stage + ': ' + tierLabel; 71 return window.LM_LANDING_VARIANT ? (base + ' (' + window.LM_LANDING_VARIANT + ')') : base; 72 }; 73 74 // Which area of the site is this page in? (sent as a property for breakdowns) 75 function section() { 76 var p = location.pathname.toLowerCase(); 77 if (p.indexOf('/german-stories/') === 0) return 'german-stories'; 78 if (p.indexOf('/beginner/') === 0) return 'beginner'; 79 if (p.indexOf('/kids/') === 0) return 'kids'; 80 if (p.indexOf('/german-grammar/') === 0) return 'grammar-qa'; 81 if (p.indexOf('/membership/') === 0) return 'membership'; 82 if (p.indexOf('/flashcards/') === 0) return 'flashcards'; 83 if (p.indexOf('/printables/') === 0) return 'printables'; 84 if (p.indexOf('/auth/') === 0) return 'auth'; 85 if (/\/chapter\d+\.php$/.test(p)) return 'book-chapter'; 86 if (p === '/' || p === '/index.php') return 'home'; 87 return 'other'; 88 } 89 var SECTION = section(); 90 91 // Which of home.php's 3 shuffled landing pages actually rendered (see 92 // includes/header.php - all 3 can be served at the same "/" URL via 93 // home.php's server-side require(), so Plausible's automatic URL-based 94 // pageview tracking can't tell them apart on its own). Fired as its OWN 95 // event name per page (not one shared event name + a property) so each 96 // page's count shows up as its own Goal in Plausible on every plan tier - 97 // property-based breakdowns need the paid Business plan, a separate 98 // event name doesn't. 99 if (window.LM_LANDING_VARIANT) { 100 track('Landing View: ' + window.LM_LANDING_VARIANT); 101 } 102 103 // Shared helper: where a paid checkout should send the buyer so the per-plan 104 // thank-you page fires the purchase conversion, then forwards to the receipt. 105 // pro â /pro-thankyou.php 106 // platinum (new) â /platinum-thankyou.php 107 // platinum (from pro/premium) â /upgrade-thankyou.php 108 // The receipt URL carries &ty=1 so the generic Purchase below won't double-fire. 109 window.lmThankYouUrl = function (tier, currentTier) { 110 tier = String(tier || '').toLowerCase(); 111 currentTier = String(currentTier || '').toLowerCase(); 112 var page = (tier === 'platinum') 113 ? ((currentTier === 'pro' || currentTier === 'premium') ? '/upgrade-thankyou.php' : '/platinum-thankyou.php') 114 : '/pro-thankyou.php'; 115 return page + '?next=' + encodeURIComponent('/membership/success.php?tier=' + tier + '&ty=1'); 116 }; 117 118 // Conversion endpoint: a success / thank-you page load (root or membership). 119 // Skip when ?ty=1 â a thank-you page already fired the specific purchase event 120 // and forwarded here, so firing again would double-count.
121 // Two different conversions land here, so the event name says which: 122 // /success.php â standalone book purchase (buy.php's checkout) 123 // /membership/success.php â membership purchase, but ONLY as a defensive 124 // fallback (see includes/analytics.php's 125 // lmThankYouUrl / the pro-thankyou.php family) â 126 // the real membership conversion event is fired 127 // by includes/thankyou.php itself. 128 if (/success\.php$/.test(location.pathname.toLowerCase()) && location.search.indexOf('ty=1') === -1) { 129 var purchaseLabel = (location.pathname.toLowerCase().indexOf('/membership/') === 0) ? 'Purchase: Membership (fallback)' : 'Purchase: Book'; 130 track(purchaseLabel, { section: SECTION }); 131 } 132 133 // Delegated click tracking (capture phase, so it also catches buttons whose 134 // own handlers stop propagation, and elements added to the page later). 135 document.addEventListener('click', function (e) { 136 var el = e.target; 137 if (!el || !el.closest) return; 138 139 // Generic tagging hook: <a data-evt="Event Name" data-evt-foo="bar"> ⦠140 var tagged = el.closest('[data-evt]'); 141 if (tagged) { 142 var props = {}; 143 Array.prototype.forEach.call(tagged.attributes, function (a) { 144 if (a.name.indexOf('data-evt-') === 0) props[a.name.slice(9)] = a.value; 145 }); 146 track(tagged.getAttribute('data-evt'), Object.keys(props).length ? props : { section: SECTION }); 147 } 148 149 // Audio Play â story readers (#lm-play-btn) and kids reader (#btn-play / #bottom-play-pause) 150 if (el.closest('#lm-play-btn, #btn-play, #bottom-play-pause')) { 151 track('Audio Play', { section: SECTION }); 152 } 153 154 // Save Progress â the reader "Save Progress" button 155 if (el.closest('[onclick*="saveStoryProgress"]')) { 156 track('Save Progress', { section: SECTION }); 157 } 158 159 // Upgrade Click â links toward the pricing / upgrade / membership-plans page 160 // (deliberately excludes /membership/dashboard.php, manage.php, success.php â¦) 161 // On a shuffle landing page, fired as its own per-page event name (same 162 // reasoning as "Landing View: X" above) so click-through is countable 163 // per page without needing Plausible's paid plan; elsewhere on the site 164 // it's the original shared "Upgrade Click" event. 165 var link = el.closest('a[href]'); 166 if (link) { 167 var href = (link.getAttribute('href') || '').toLowerCase(); 168 if (/pricing|upgrade/.test(href) || /\/membership\/(index\.php)?(\?|#|$)/.test(href)) { 169 var evtName = window.LM_LANDING_VARIANT ? ('Upgrade Click: ' + window.LM_LANDING_VARIANT) : 'Upgrade Click'; 170 track(evtName, { from: SECTION }); 171 } 172 } 173 }, true); 174})(); 175</script>
175 176</head> 177<body class="min-h-screen flex flex-col items-center justify-center p-2 sm:p-4 relative overflow-hidden"> 178<!-- Google Tag Manager (noscript) -->
vendor: 68 bytes, lines 178-179
178 179<noscript><iframe src="https://www.googletagmanager.com/ns.html?id=
179GTM-5TCHGBTM
vendor: 83 bytes, lines 179-180
179" 180height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>
181<!-- End Google Tag Manager (noscript) --> 182 183 <!-- Decorative scattered book covers â purely visual, behind the card. --> 184 <div class="fixed inset-0 overflow-hidden pointer-events-none select-none" aria-hidden="true"> 185 <div class="absolute inset-0 bg-slate-100"></div> 186 <img src="/german-stories/images/covers/eine-baerenstarke-liebe-ostlich-vom-spass-und-westlich-vom-mond.webp" alt="" loading="eager" 187 class="absolute rounded-xl shadow-lg opacity-40" 188 style="left:1%; top:0%; width:101px; transform: rotate(-7deg);"> 189 <img src="/german-stories/images/covers/in-yankee-in-camelot-eine-zeitreise-der-besonderen-art.webp" alt="" loading="eager" 190 class="absolute rounded-xl shadow-lg opacity-40" 191 style="left:17.666666666667%; top:-4%; width:134px; transform: rotate(0deg);"> 192 <img src="/german-stories/images/covers/eine-bruecke-bauen-annas-und-marks-weg-zur-versoehnung.webp" alt="" loading="eager" 193 class="absolute rounded-xl shadow-lg opacity-40" 194 style="left:34.333333333333%; top:2%; width:102px; transform: rotate(-7deg);"> 195 <img src="/german-stories/images/covers/michael-kohlhaas.webp" alt="" loading="eager" 196 class="absolute rounded-xl shadow-lg opacity-40" 197 style="left:48%; top:-4%; width:113px; transform: rotate(-4deg);"> 198 <img src="/german-stories/images/covers/die-verwandlung.webp" alt="" loading="eager" 199 class="absolute rounded-xl shadow-lg opacity-40" 200 style="left:68.666666666667%; top:4%; width:117px; transform: rotate(4deg);"> 201 <img src="/german-stories/images/covers/der-schimmelreiter.webp" alt="" loading="eager" 202 class="absolute rounded-xl shadow-lg opacity-40" 203 style="left:83.333333333333%; top:4%; width:97px; transform: rotate(12deg);"> 204 <img src="/german-stories/images/covers/mitteilungen-aus-den-memoiren-des-satan.webp" alt="" loading="eager" 205 class="absolute rounded-xl shadow-lg opacity-40" 206 style="left:3%; top:28%; width:117px; transform: rotate(-6deg);"> 207 <img src="/german-stories/images/covers/das-spiel-des-lebens-und-wie-man-es-spielt.webp" alt="" loading="eager" 208 class="absolute rounded-xl shadow-lg opacity-40" 209 style="left:19.666666666667%; top:28%; width:136px; transform: rotate(-6deg);"> 210 <img src="/german-stories/images/covers/uriel-acosta.webp" alt="" loading="eager" 211 class="absolute rounded-xl shadow-lg opacity-40" 212 style="left:30.333333333333%; top:27%; width:130px; transform: rotate(-6deg);"> 213 <img src="/german-stories/images/covers/aus-ruinen-zur-republik.webp" alt="" loading="eager" 214 class="absolute rounded-xl shadow-lg opacity-40" 215 style="left:53%; top:23%; width:92px; transform: rotate(-1deg);"> 216 <img src="/german-stories/images/covers/tom-sawyer-und-der-gestrichene-zaun.webp" alt="" loading="eager" 217 class="absolute rounded-xl shadow-lg opacity-40" 218 style="left:63.666666666667%; top:26%; width:131px; transform: rotate(-4deg);"> 219 <img src="/german-stories/images/covers/rapunzel-eine-lustige-geschichte.webp" alt="" loading="eager" 220 class="absolute rounded-xl shadow-lg opacity-40" 221 style="left:85.333333333333%; top:23%; width:102px; transform: rotate(-11deg);"> 222 <img src="/german-stories/images/covers/die-odyssee-der-konnektivitaet-ein-abenteuer-in-deutschland.webp" alt="" loading="eager" 223 class="absolute rounded-xl shadow-lg opacity-40" 224 style="left:-3%; top:49%; width:130px; transform: rotate(-12deg);"> 225 <img src="/german-stories/images/covers/der-unsichtbare-mann.webp" alt="" loading="eager" 226 class="absolute rounded-xl shadow-lg opacity-40" 227 style="left:14.666666666667%; top:51%; width:111px; transform: rotate(-12deg);"> 228 <img src="/german-stories/images/covers/die-dame-des-sees.webp" alt="" loading="eager"
229 class="absolute rounded-xl shadow-lg opacity-40" 230 style="left:34.333333333333%; top:52%; width:106px; transform: rotate(0deg);"> 231 <img src="/german-stories/images/covers/das-schloss-von-otranto.webp" alt="" loading="eager" 232 class="absolute rounded-xl shadow-lg opacity-40" 233 style="left:51%; top:54%; width:128px; transform: rotate(5deg);"> 234 <img src="/german-stories/images/covers/die-prinzessin-und-die-sehr-sehr-kleine-erbse.webp" alt="" loading="eager" 235 class="absolute rounded-xl shadow-lg opacity-40" 236 style="left:65.666666666667%; top:50%; width:92px; transform: rotate(5deg);"> 237 <img src="/german-stories/images/covers/isabella-von-aegypten.webp" alt="" loading="eager" 238 class="absolute rounded-xl shadow-lg opacity-40" 239 style="left:81.333333333333%; top:53%; width:115px; transform: rotate(5deg);"> 240 <img src="/german-stories/images/covers/der-untertan.webp" alt="" loading="eager" 241 class="absolute rounded-xl shadow-lg opacity-40" 242 style="left:3%; top:73%; width:129px; transform: rotate(8deg);"> 243 <img src="/german-stories/images/covers/das-leben-napoleons-bonapartes-aufstieg-und-fall-eines-kaisers.webp" alt="" loading="eager" 244 class="absolute rounded-xl shadow-lg opacity-40" 245 style="left:15.666666666667%; top:72%; width:103px; transform: rotate(2deg);"> 246 <img src="/german-stories/images/covers/der-krieg-der-welten-eine-deutsche-adaption.webp" alt="" loading="eager" 247 class="absolute rounded-xl shadow-lg opacity-40" 248 style="left:34.333333333333%; top:76%; width:125px; transform: rotate(8deg);"> 249 <img src="/german-stories/images/covers/der-froschkoenig.webp" alt="" loading="eager" 250 class="absolute rounded-xl shadow-lg opacity-40" 251 style="left:51%; top:78%; width:120px; transform: rotate(-2deg);"> 252 <img src="/german-stories/images/covers/die-reise-der-seewolf.webp" alt="" loading="eager" 253 class="absolute rounded-xl shadow-lg opacity-40" 254 style="left:69.666666666667%; top:76%; width:114px; transform: rotate(11deg);"> 255 <img src="/german-stories/images/covers/schneewittchen-und-der-sprechende-spiegel.webp" alt="" loading="eager" 256 class="absolute rounded-xl shadow-lg opacity-40" 257 style="left:81.333333333333%; top:77%; width:101px; transform: rotate(6deg);"> 258 <div class="absolute inset-0 bg-gradient-to-b from-white/70 via-white/50 to-white/75"></div> 259 </div> 260 261 <div class="max-w-md w-full bg-white rounded-3xl shadow-xl border border-slate-200 overflow-hidden relative z-10"> 262 263 <!-- Header & Logo --> 264 <div class="bg-slate-900 px-5 py-5 sm:px-8 sm:py-10 text-center text-white"> 265 266 <!-- LOGO SECTION --> 267 <div class="flex justify-center items-center gap-2 mb-0 sm:mb-6"> 268 <img src="/auth/logo.png" alt="Logo" class="h-8"> 269 270 <span class="text-2xl font-black tracking-tight text-white">Vajo.com</span> 271 </div> 272 273 <!-- Hidden on tiny phones to keep the Log In button inside the viewport â --> 274 <!-- the logo above already identifies the page. --> 275 <h1 id="formTitle" class="hidden sm:block text-3xl font-extrabold tracking-tight mb-2">Welcome Back</h1> 276 <p id="formSubtitle" class="hidden sm:block text-slate-400">Log in to continue your German journey.</p> 277 </div> 278 279 <div class="p-5 sm:p-8"> 280 <!-- URL Alerts --> 281 <div class="mb-6 bg-blue-50 text-blue-700 p-4 rounded-xl text-sm font-bold text-center"> 282 Please create a free account or log in to access this material. </div> 283 284 <!-- JS Error Alerts --> 285 <div id="alertBox" class="hidden mb-6 p-4 rounded-xl text-sm font-bold text-center border"></div> 286 287 <form id="authForm" onsubmit="handleAuth(event)" class="space-y-3 sm:space-y-5"> 288 289 <div> 290 <label class="block text-sm font-bold text-slate-700 mb-1">Email Address</label> 291 <input type="email" id="email" required placeholder="[email protected]" oninput="checkCompEmail()" value="" 292 class="w-full px-4 py-2.5 sm:py-3 rounded-xl border border-slate-200 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none transition-all bg-slate-50 focus:bg-white"> 293 <p id="compHint" class="hidden mt-2 text-xs font-bold text-emerald-600">â Complimentary teacher pass detected â no password needed. Just click âLog Inâ.</p> 294 </div> 295 296 <div id="passwordFieldContainer"> 297 <div class="flex justify-between items-center mb-1"> 298 <label class="block text-sm font-bold text-slate-700">Password</label> 299 <button type="button" onclick="setMode('forgot')" id="forgotLink" class="text-xs font-bold text-blue-600 hover:text-blue-800 transition-colors">Forgot password?</button> 300 </div> 301 <div class="relative"> 302 <input type="password" id="password" required placeholder="â¢â¢â¢â¢â¢â¢â¢â¢" minlength="6" 303 class="w-full px-4 py-2.5 sm:py-3 rounded-xl border border-slate-200 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none transition-all bg-slate-50 focus:bg-white pr-12"> 304 305 <!-- Show/Hide Password Toggle Button --> 306 <button type="button" onclick="togglePassword()" class="absolute right-4 top-1/2 -translate-y-1/2 text-slate-400 hover:text-slate-600 focus:outline-none transition-colors" title="Toggle password visibility"> 307 <svg id="eyeOpen" class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
307<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"></path></svg> 308 <svg id="eyeClosed" class="w-5 h-5 hidden" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21"></path></svg> 309 </button> 310 </div> 311 </div> 312 313 <!-- Terms & Privacy (register mode only) â matches includes/signup-gate.php's 314 popup, which has always required this; this standalone form never did. --> 315 <div id="termsContainer" class="hidden space-y-2"> 316 <label class="flex items-start gap-2 text-xs text-slate-600 cursor-pointer"> 317 <input type="checkbox" id="termsAccepted" class="mt-0.5 w-4 h-4 text-blue-600 bg-slate-50 border-slate-300 rounded focus:ring-blue-500 focus:ring-2 cursor-pointer shrink-0"> 318 <span>I accept the <a href="/terms.php" onclick="openLegal(event,'/terms.php','Terms');" class="text-blue-600 underline">Terms and Conditions</a></span> 319 </label> 320 <label class="flex items-start gap-2 text-xs text-slate-600 cursor-pointer"> 321 <input type="checkbox" id="privacyAccepted" class="mt-0.5 w-4 h-4 text-blue-600 bg-slate-50 border-slate-300 rounded focus:ring-blue-500 focus:ring-2 cursor-pointer shrink-0"> 322 <span>I accept the <a href="/privacy.php" onclick="openLegal(event,'/privacy.php','Privacy');" class="text-blue-600 underline">Privacy Policy</a></span> 323 </label> 324 </div> 325 326 <!-- Stay Logged In Checkbox --> 327 <div id="rememberMeContainer" class="flex items-center -mt-2"> 328 <input id="rememberMe" type="checkbox" class="w-4 h-4 text-blue-600 bg-slate-50 border-slate-300 rounded focus:ring-blue-500 focus:ring-2 cursor-pointer transition-colors"> 329 <label for="rememberMe" class="ml-2 text-sm font-medium text-slate-600 select-none cursor-pointer">Stay logged in</label> 330 </div> 331 332 <!-- Google reCAPTCHA Widget --> 333 <div class="pt-1 pb-1 sm:pt-2 sm:pb-2 flex justify-center"> 334 <div class="g-recaptcha" data-sitekey="6LettZ8tAAAAAHjnfNu9S421PZVs_iQfiiDrVKFh"></div> 335 </div> 336 337 <button type="submit" id="submitBtn" class="w-full bg-blue-600 hover:bg-blue-700 text-white font-extrabold py-3 sm:py-3.5 rounded-xl shadow-md hover:shadow-lg transition-all"> 338 Log In 339 </button> 340 341 </form> 342 343 <!-- Mode Toggle --> 344 <div class="mt-4 sm:mt-8 text-center border-t border-slate-100 pt-4 sm:pt-6"> 345 <p class="text-sm text-slate-500"> 346 <span id="toggleText">Don't have an account?</span> 347 <button onclick="setMode(currentMode === 'login' ? 'register' : 'login')" class="font-bold text-blue-600 hover:text-blue-800 transition-colors ml-1" type="button" id="toggleBtn"> 348 Sign up for free 349 </button> 350 </p> 351 </div> 352 353 <div class="mt-2 sm:mt-4 text-center"> 354 <a href="/german-stories/index.php" class="text-xs font-bold text-slate-400 hover:text-slate-600 transition-colors">← Back to Home</a> 355 <span class="text-slate-300 mx-1">·</span> 356 <a href="/affiliate/become-affiliate.php" class="text-xs font-bold text-slate-400 hover:text-slate-600 transition-colors">Become an Affiliate (41%)</a> 357 </div> 358 359 </div> 360 </div> 361 362
362<script> 363 let currentMode = 'login'; // 'login', 'register', or 'forgot' 364 let forceLoginActive = false; // State tracker for 5-device limit 365 const LOGIN_REDIRECT = ""; // '' unless a valid ?redirect= was given 366 367 // Complimentary teacher pass: a passwordless shared account. When its email 368 // is entered (in login mode) we hide the password field and let the user 369 // log in with just the email. 370 const COMP_EMAILS = ['[email protected]', '[email protected]']; 371 let compMode = false; 372 373 function checkCompEmail() { 374 const email = (document.getElementById('email').value || '').trim().toLowerCase(); 375 const isComp = COMP_EMAILS.includes(email); 376 const pwWrap = document.getElementById('passwordFieldContainer'); 377 const pwInput = document.getElementById('password'); 378 const hint = document.getElementById('compHint'); 379 380 // Reachable via ?mode=register deep links or the Sign Up toggle â 381 // auto-switch to Login so the password field actually hides. 382 // setMode() re-runs checkCompEmail() with currentMode already 383 // updated, so this doesn't recurse. 384 if (isComp && currentMode !== 'login') { 385 setMode('login'); 386 return; 387 } 388 389 if (isComp && currentMode === 'login') { 390 compMode = true; 391 pwWrap.classList.add('hidden'); 392 pwInput.required = false; 393 hint.classList.remove('hidden'); 394 } else { 395 compMode = false; 396 hint.classList.add('hidden'); 397 if (currentMode === 'login') { 398 pwWrap.classList.remove('hidden'); 399 pwInput.required = true; 400 } 401 } 402 } 403 404 // Real secondary browser window with scrollbars â matches 405 // includes/signup-gate.php's lmSgOpenLegal() exactly. 406 function openLegal(e, url, name) { 407 e.preventDefault(); 408 window.open(url, name, 'width=600,height=700,scrollbars=yes,resizable=yes'); 409 } 410 411 function togglePassword() { 412 const pwdInput = document.getElementById('password'); 413 const eyeOpen = document.getElementById('eyeOpen'); 414 const eyeClosed = document.getElementById('eyeClosed'); 415 416 if (pwdInput.type === 'password') { 417 pwdInput.type = 'text'; 418 eyeOpen.classList.add('hidden'); 419 eyeClosed.classList.remove('hidden'); 420 } else { 421 pwdInput.type = 'password'; 422 eyeOpen.classList.remove('hidden'); 423 eyeClosed.classList.add('hidden'); 424 } 425 } 426 427 function setMode(mode) { 428 currentMode = mode; 429 forceLoginActive = false; // Reset the force login state if they click around 430 431 const formTitle = document.getElementById('formTitle'); 432 const formSubtitle = document.getElementById('formSubtitle'); 433 const submitBtn = document.getElementById('submitBtn'); 434 const toggleText = document.getElementById('toggleText'); 435 const toggleBtn = document.getElementById('toggleBtn'); 436 const alertBox = document.getElementById('alertBox'); 437 438 const passwordFieldContainer = document.getElementById('passwordFieldContainer'); 439 const rememberMeContainer = document.getElementById('rememberMeContainer'); 440 const termsContainer = document.getElementById('termsContainer'); 441 const forgotLink = document.getElementById('forgotLink'); 442 const passwordInput = document.getElementById('password'); 443 444 // Clear any existing errors when switching 445 alertBox.classList.add('hidden'); 446 447 // Reset classes for the submit button to baseline 448 const baseBtnClasses = "w-full text-white font-extrabold py-3.5 rounded-xl shadow-md hover:shadow-lg transition-all "; 449 450 if (mode === 'register') { 451 formTitle.textContent = "Create Account"; 452 formSubtitle.textContent = "Sign up free and get 24 hours of Platinum access."; 453 submitBtn.textContent = "Sign Up for 24H Free Platinum Level Access"; 454 submitBtn.className = baseBtnClasses + "bg-slate-900 hover:bg-slate-800"; 455 456 toggleText.textContent = "Already have an account?"; 457 toggleBtn.textContent = "Log in"; 458 459 passwordFieldContainer.classList.remove('hidden'); 460 rememberMeContainer.classList.add('hidden'); // Hide on register 461 termsContainer.classList.remove('hidden'); 462 forgotLink.classList.add('hidden'); 463 passwordInput.required = true; 464 465 } else if (mode === 'forgot') { 466 formTitle.textContent = "Reset Password"; 467 formSubtitle.textContent = "Enter your email to receive a reset link."; 468 submitBtn.textContent = "Send Reset Link"; 469 submitBtn.className = baseBtnClasses + "bg-blue-600 hover:bg-blue-700"; 470 471 toggleText.textContent = "Remembered your password?"; 472 toggleBtn.textContent = "Log in"; 473 474 // Hide password requirement for reset flow 475 passwordFieldContainer.classList.add('hidden'); 476 rememberMeContainer.classList.add('hidden'); // Hide on forgot password 477 termsContainer.classList.add('hidden'); 478 passwordInput.required = false;
479 480 } else { 481 // Default Login Mode 482 formTitle.textContent = "Welcome Back"; 483 formSubtitle.textContent = "Log in to continue your German journey."; 484 submitBtn.textContent = "Log In"; 485 submitBtn.className = baseBtnClasses + "bg-blue-600 hover:bg-blue-700"; 486 487 toggleText.textContent = "Don't have an account?"; 488 toggleBtn.textContent = "Sign up for free"; 489 490 passwordFieldContainer.classList.remove('hidden'); 491 rememberMeContainer.classList.remove('hidden'); // Show on login 492 termsContainer.classList.add('hidden'); 493 forgotLink.classList.remove('hidden'); 494 passwordInput.required = true; 495 } 496 497 // Re-sync the passwordless comp state for the new mode (e.g. leaving 498 // login mode must restore the password field). 499 checkCompEmail(); 500 501 // Reset reCAPTCHA in case they clicked it before switching modes 502 if (typeof grecaptcha !== 'undefined') { 503 grecaptcha.reset(); 504 } 505 } 506 507 async function handleAuth(e) { 508 e.preventDefault(); 509 const btn = document.getElementById('submitBtn'); 510 const alertBox = document.getElementById('alertBox'); 511 512 // Grab the reCAPTCHA token generated by Google 513 const recaptchaResponse = grecaptcha.getResponse(); 514 515 if (!recaptchaResponse) { 516 showAlert('Please complete the "I am not a robot" check.', 'error'); 517 return; 518 } 519 520 // Matches includes/signup-gate.php's popup, which has always 521 // required this; this standalone form never checked it. 522 if (currentMode === 'register') { 523 const termsOk = document.getElementById('termsAccepted').checked; 524 const privacyOk = document.getElementById('privacyAccepted').checked; 525 if (!termsOk || !privacyOk) { 526 showAlert('Please accept the Terms and Privacy Policy to continue.', 'error'); 527 return; 528 } 529 } 530 531 const data = { 532 email: document.getElementById('email').value, 533 recaptcha_token: recaptchaResponse 534 }; 535 536 // Only grab the password if we aren't submitting a forgot password request 537 if (currentMode !== 'forgot') { 538 data.password = document.getElementById('password').value; 539 } 540 541 // Handle specific Login variables (like Force Login overrides) 542 if (currentMode === 'login') { 543 data.stay_logged_in = document.getElementById('rememberMe').checked; 544 data.force_login = forceLoginActive; 545 } 546 547 if (currentMode === 'register') { 548 data.terms_accepted = document.getElementById('termsAccepted').checked; 549 data.privacy_accepted = document.getElementById('privacyAccepted').checked; 550 } 551 552 // Loading state 553 const originalText = btn.textContent; 554 btn.disabled = true; 555 btn.textContent = 'Please wait...';
556 btn.classList.add('opacity-70', 'cursor-not-allowed'); 557 alertBox.classList.add('hidden'); 558 559 try { 560 const res = await fetch(`auth.php?action=${currentMode}`, { 561 method: 'POST', 562 headers: { 'Content-Type': 'application/json' }, 563 body: JSON.stringify(data) 564 }); 565 566 const result = await res.json(); 567 568 if (result.success) { 569 if (currentMode === 'forgot') { 570 showAlert(result.message || "If an account exists, a reset link has been sent.", 'success'); 571 grecaptcha.reset(); 572 } else if (currentMode === 'register') { 573 // New free account â free thank-you page (fires the sign_up 574 // conversion), which then forwards to the membership page. 575 window.location.href = '/free-thankyou.php'; 576 } else if (result.complimentary) { 577 // Complimentary teacher pass â 3-second welcome screen â dashboard. 578 window.location.href = '/membership/complimentary-welcome.php'; 579 } else if (LOGIN_REDIRECT) { 580 window.location.href = LOGIN_REDIRECT; 581 } else { 582 const tier = (result.tier || 'free').toLowerCase(); 583 window.location.href = tier === 'platinum' ? '/membership/dashboard.php' : '/membership/index.php'; 584 } 585 } else { 586 // Check if they hit the 5 device limit 587 if (result.error_code === 'DEVICE_LIMIT_REACHED') { 588 forceLoginActive = true; 589 590 // Transform the submit button 591 btn.classList.remove('bg-blue-600', 'hover:bg-blue-700'); 592 btn.classList.add('bg-red-600', 'hover:bg-red-700'); 593 594 // Show warning prompt 595 alertBox.innerHTML = `<strong>5 Device Limit Reached!</strong><br>Check the CAPTCHA box again and click the red button below to securely sign out of all other devices.`; 596 alertBox.className = `mb-6 p-4 rounded-xl text-sm text-center border block bg-amber-50 text-amber-800 border-amber-200`; 597 598 grecaptcha.reset(); 599 } else if (result.error_code === 'COMP_EXPIRED') { 600 // Hard stop passed â tell them and send them to the signup page. 601 showAlert('Sorry, your 7 day complimentary pass has expired. You will now be directed to the signup page.', 'error'); 602 grecaptcha.reset(); 603 setTimeout(function () { window.location.href = '/auth/login.php?mode=register&msg=comp_expired'; }, 3000); 604 } else { 605 showAlert(result.message, 'error'); 606 grecaptcha.reset(); 607 } 608 } 609 } catch (err) { 610 showAlert('A network error occurred. Please try again.', 'error'); 611 grecaptcha.reset(); 612 } 613 614 // Restore button 615 btn.disabled = false;
616 btn.classList.remove('opacity-70', 'cursor-not-allowed'); 617 618 // If they are in Force Login mode, keep the button text accurately updated 619 if (forceLoginActive && currentMode === 'login') { 620 btn.textContent = 'Sign Out Others & Log In'; 621 } else { 622 btn.textContent = originalText; 623 } 624 } 625 626 function showAlert(message, type) { 627 const alertBox = document.getElementById('alertBox'); 628 alertBox.textContent = message; // Overwrites any innerHTML (like the device limit prompt) 629 alertBox.className = `mb-6 p-4 rounded-xl text-sm font-bold text-center border block`; 630 631 if (type === 'error') { 632 alertBox.classList.add('bg-red-50', 'text-red-700', 'border-red-200'); 633 } else { 634 alertBox.classList.add('bg-green-50', 'text-green-700', 'border-green-200'); 635 } 636 } 637 638 // Open in the requested starting mode (e.g. arriving as the "signup page" 639 // after a complimentary pass expires), then sync the passwordless state. 640 (function () { 641 var startMode = "login"; 642 if (startMode === 'register' || startMode === 'forgot') { setMode(startMode); } 643 else { checkCompEmail(); } 644 })(); 645 </script>
645 646</body> 647</html>
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.