1<!DOCTYPE html> 2<html lang="de"> 3<head> 4 5 <meta charset="utf-8"/> 6 <meta name="viewport" content="width=device-width, initial-scale=1"> 7 <meta name="description" content="Deutschunterricht. Einfach. Digital. - Durch einfache Zuweisung von Grammatikaufgaben und automatische Korrektur den Unterricht digital gestalten. Zeitsparend. Ãbersichtlich."> 8 9 <title>Grammatip</title> 10 11
11<script src="/javascripts/cookie-helper.js?v=1655670100"></script>
11 12 <link rel="shortcut icon" href="/favicon.ico?v=1782478304" type="image/x-icon" /> 13 14 <!-- PWA Start --> 15<!-- General --> 16<meta data-hid="mobile-web-app-capable" name="mobile-web-app-capable" content="yes"> 17<meta data-hid="og:type" name="og:type" property="og:type" content="website"> 18<meta data-hid="og:title" name="og:title" property="og:title" content="Grammatip"> 19<meta data-hid="og:site_name" name="og:site_name" property="og:site_name" content="Grammatip"> 20<link rel="icon" type="image/x-icon" href="/favicon.ico"> 21<link data-hid="shortcut-icon" rel="shortcut icon" href="/android-chrome-192x192.png"> 22 23<!-- Apple --> 24<meta data-hid="apple-mobile-web-app-title" name="apple-mobile-web-app-title" content="Grammatip"> 25<meta name="theme-color" content="#2D57FC"/> 26<meta name="apple-mobile-web-app-status-bar" content="#2D57FC"/> 27<link data-hid="apple-touch-icon" rel="apple-touch-icon" href="/android-chrome-512x512.png" sizes="512x512"> 28 29<!-- Windows --> 30<meta name="application-name" content="Grammatip"/> 31<meta name="msapplication-TileImage" content="/android-chrome-512x512.png"> 32<meta name="msapplication-TileColor" content="#2D57FC"> 33 34<!-- Manifest --> 35<link rel="manifest" href="/manifest.json" data-hid="manifest" crossorigin="use-credentials"> 36 37<!-- Service worker -->
38<script> 39 40 window.serviceWorkerSupported = true; 41 42 function isIOS16() { 43 return ( 44 navigator.appVersion.includes('Version/16') || 45 navigator.appVersion.includes('OS 16') 46 ) && ( 47 /iPhone/.test(navigator.userAgent) || 48 /iPad/.test(navigator.userAgent) || 49 /iPod/.test(navigator.userAgent) || 50 /Macintosh/.test(navigator.userAgent) 51 ); 52 } 53 54 55 /** 56 * @param tryOnce 57 * @returns {Promise<void|ServiceWorker|*>} 58 * 59 * @see https://stackoverflow.com/a/67612740 60 */ 61 async function registerServiceWorker(tryOnce = false) { 62 if (isIOS16()) { 63 window.serviceWorkerSupported = false; 64 65 try { 66 const reg = await navigator.serviceWorker.getRegistration('/'); 67 await reg.unregister(); 68 } catch (err) { 69 return; 70 } 71 72 return; 73 } 74 75 if (!('serviceWorker' in navigator)) { 76 window.serviceWorkerSupported = false; 77 throw new Error('serviceWorker not supported'); 78 } 79 80 const url = (new URL(`/serviceWorker.js?v=g8534h`, location)).toString(); 81 //console.info('Registering worker'); 82 const registration = await navigator.serviceWorker.register(url, { 83 scope: '/', 84 }); 85 86 const registeredWorker = registration.active || registration.waiting || registration.installing; 87 //console.info('Registered worker:', registeredWorker); 88 if (registeredWorker?.scriptURL != url) { 89 console.log('[ServiceWorker] Old URL:', registeredWorker?.scriptURL || 'none', 'updating to:', url); 90 await registration.update(); 91 //console.info('Updated worker'); 92 } 93 94 //console.info('Waiting for ready worker'); 95 let serviceReg = await navigator.serviceWorker.ready; 96 //console.info('Ready registration:', serviceReg); 97 98 if (!navigator.serviceWorker.controller) { 99 //console.info('Worker isnât controlling, re-register'); 100 try { 101 const reg = await navigator.serviceWorker.getRegistration('/'); 102 //console.info('Unregistering worker'); 103 await reg.unregister(); 104 //console.info('Successfully unregistered, trying registration again'); 105 return registerServiceWorker(); 106 } catch (err) { 107 window.serviceWorkerSupported = false; 108 console.error(`ServiceWorker failed to re-register after hard-refresh, reloading the page!`, err); 109 return location.reload(); 110 } 111 } 112 113 let serviceWorker = serviceReg.active || serviceReg.waiting || serviceReg.installing; 114 if (!serviceWorker) { 115 //console.info('No worker on registration, getting registration again'); 116 serviceReg = await navigator.serviceWorker.getRegistration('/'); 117 serviceWorker = serviceReg.active || serviceReg.waiting || serviceReg.installing; 118 } 119 120 if (!serviceWorker) { 121 //console.info('No worker on registration, waiting 50ms'); 122 await sleep(50); // adjustable or skippable, have a play around 123 } 124 125 serviceWorker = serviceReg.active || serviceReg.waiting || serviceReg.installing; 126 if (!serviceWorker) { 127 window.serviceWorkerSupported = false; 128 throw new Error('after waiting on .ready, still no worker'); 129 } 130 131 if (serviceWorker.state == 'redundant') { 132 //console.info('Worker is redundant, trying again'); 133 return registerServiceWorker(); 134 } 135 136 if (serviceWorker.state != 'activated') { 137 //console.info('Worker IS controlling, but not active yet, waiting on event. state=', serviceWorker.state); 138 try { 139 // timeout is adjustable, but you do want one in case the statechange 140 // doesn't fire / with the wrong state because it gets queued, 141 // see ServiceWorker.onstatechange MDN docs. 142 await timeout(100, new Promise((resolve) => { 143 serviceWorker.addEventListener('statechange', (e) => { 144 if (e.target.state == 'activated') resolve(); 145 }); 146 })); 147 } catch (err) { 148 if (err instanceof TimeoutError) { 149 if (serviceWorker.state != 'activated') { 150 if (tryOnce) { 151 //console.info('Worker is still not active. state=', serviceWorker.state);
152 window.serviceWorkerSupported = false; 153 throw new Error('failed to activate service worker'); 154 } else { 155 //console.info('Worker is still not active, retrying once'); 156 return registerServiceWorker(true); 157 } 158 } 159 } else { 160 // should be unreachable 161 throw err; 162 } 163 } 164 } 165 166 //console.info('Worker is controlling and active, weâre good folks!'); 167 return serviceWorker; 168 } 169 170 class TimeoutError extends Error { 171 } 172 173 /** 174 * Run promise but reject after some timeout. 175 * 176 * @template T 177 * @param {number} ms Milliseconds until timing out 178 * @param {Promise<T>} promise Promise to run until timeout (note that it will keep running after timeout) 179 * @returns {Promise<T, Error>} 180 */ 181 function timeout(ms, promise) { 182 return new Promise((resolve, reject) => { 183 const timer = setTimeout(() => { 184 reject(new TimeoutError); 185 }, ms); 186 187 promise.then((result) => { 188 clearTimeout(timer); 189 resolve(result); 190 }, (error) => { 191 clearTimeout(timer); 192 reject(error); 193 }); 194 }) 195 } 196 197 registerServiceWorker() 198 199</script>
199 200<!-- PWA End --> 201 202 203 <link rel="stylesheet" href="/assets/new/css/web-app.css?id=be3c8f791d151789a2b9627dc861ecbe" type="text/css"> 204 205 206 207
208<script> 209 var analyticsCookies = ['_gcl_au', '_ga', '_gid', '_gat_UA-630585-12']; 210 for (var i in analyticsCookies) { 211 if (!getCookieValue(analyticsCookies[i])) { 212 continue; 213 } 214 215 document.cookie = analyticsCookies[i] + '=; path=/; domain=; expires=Thu, 08 Jan 1970 00:00:01 GMT'; 216 } 217</script>
217 218 219</head> 220<body class="landing-page-body "> 221 222<div id="vue-app"> 223 <cookies-consent-popup></cookies-consent-popup> 224 225 226<div class="home-container"> 227 <nav class="navbar sticky-top bg-white landing-page-header"> 228 <div class="container-xxl"> 229 <div class="navbar-header"> 230 <a class="navbar-brand" href="/"> 231 <span class="logo"> 232 <span class="d-none d-md-inline logo-text">grammatip.de</span> 233 </span> 234 </a> 235 </div> 236 <div class="nav"> 237 <div class="nav-item d-none d-xl-block"> 238 <a class="nav-link active" href="/function">So funktioniert Grammatip</a> 239 </div> 240 <div class="nav-item d-none d-xl-block"> 241 <a class="nav-link " href="/blog">Blog</a> 242 </div> 243 <div class="nav-item"> 244 <div class="nav-link"> 245 <a class="btn btn-outline-primary btn-lg btn-teardrop d-none d-sm-block" href="/register-school">Nutzerkonto anlegen</a> 246 <a class="btn btn-outline-primary btn-lg btn-teardrop d-sm-none" href="/register-school">Konto anlegen</a> 247 </div> 248 </div> 249 <div class="nav-item"> 250 <div class="nav-link"> 251 <a class="btn btn-primary btn-lg btn-teardrop d-none d-sm-block" href="/login">Einloggen</a> 252 <a class="btn btn-primary btn-lg btn-teardrop d-sm-none" href="/login">Einloggen</a> 253 </div> 254 </div> 255 <div class="nav-item d-xl-none"> 256 <div class="nav-link"> 257 <div class="dropdown lp-header-dropdown"> 258 <a class="btn btn-lg btn-icon dropdown-toggle flex-column menu-button" href="#" id="navbarHeaderDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false"> 259 <img src="/images/icons/dropdown-icon.svg" alt=""> 260 <span class="menu-text">MENÃ</span> 261 </a> 262 263 <ul class="dropdown-menu dropdown-menu-end" aria-labelledby="navbarHeaderDropdown"> 264 <li><a class="dropdown-item" href="/function">So funktioniert Grammatip</a></li> 265 <li><a class="dropdown-item" href="/blog">Blog</a></li> 266 </ul> 267 </div> 268 </div> 269 </div> 270 </div> 271 </div> 272</nav> 273 274 <main> 275 <div id="functions" class="container-xxl home-page-container"> 276 <div class="grammatik-online-section"> 277 <h2 class="text-32 text-sm-44 text-xl-60 lh-sm fw-bold"><span class="text-primary">So funktioniert</span><br> Grammatip</h2> 278 </div> 279 <landing-page-functions></landing-page-functions> 280 281 <div class="image-container"> 282 <img class="image-one" src="/assets/images/landing-page/image_one.png" alt="" loading="lazy"> 283 <img class="image-two" src="/assets/images/landing-page/image_two.png" alt="" loading="lazy"> 284 <img class="image-three" src="/assets/images/landing-page/image_three.png" alt="" loading="lazy"> 285 </div> 286 <div class="grammatiger"> 287 <img class="grammatiger-jump" src="/assets/images/grammatiger/grammatiger_jump.png" alt="Grammatiger" loading="lazy"> 288 <img class="grammatiger-left-hand" src="/assets/images/grammatiger/grammatiger_left_hand.png" alt="Grammatiger left hand" loading="lazy"> 289 <img class="grammatiger-right-hand" src="/assets/images/grammatiger/grammatiger_right_hand.png" alt="Grammatiger right hand" loading="lazy"> 290 </div> 291 292 </div> 293 </main> 294 <footer> 295 <div class="top-footer"> 296 <div class="container-xxl"> 297 <div class="row landing-page-content gap-3"> 298 <div class="col-auto d-flex flex-column mb-3"> 299 <a href="/"> 300 <img class="grammatip-logo me-4" src="/assets/images/landing-page/grammatip.de.svg" alt="grammatip.com logo" loading="lazy"> 301 </a> 302 <div class="d-md-flex d-none mt-1 flex-lg-row flex-wrap"> 303 <a class="btn btn-outline-primary btn-lg btn-teardrop text-nowrap me-3 mt-3 mb-lg-0" href="/function">So funktioniert Grammatip</a> 304 <a class="btn btn-outline-primary btn-lg btn-teardrop text-nowrap me-3 mt-3 mb-lg-0" href="/blog">Blog</a> 305 </div> 306 </div> 307 <div class="col flex-column mb-3 d-none d-xxl-flex"> 308 <a class="btn btn-outline-primary btn-lg btn-teardrop text-nowrap me-3 mt-3 mb-lg-0 ms-auto" href="/register-school">Konto anlegen</a> 309 <a class="btn btn-outline-primary btn-lg btn-teardrop text-nowrap me-3 mt-3 mb-lg-0 ms-auto" href="/login">Einloggen</a> 310 </div> 311 <div class="col-auto ms-xl-auto"> 312 <div class="row contact-info"> 313 <div class="col me-3 mb-3 pe-3"> 314 <p class="fw-semibold">Kontakt:</p> 315 <p> 316 <a href="mailto:[email protected]">[email protected]</a> 317 </p> 318 </div> 319 <div class="col"> 320 <ul class="policies"> 321 <li> 322 <a href="/policies/privacy">Datenschutz</a> 323 </li> 324 <li> 325 <a href="/grammatip_de_avv.pdf" target="_blank" download>Datenverarbeitungsvertrag (AVV)</a> 326 </li> 327 <li> 328 <a href="/policies/terms">AGB</a> 329 </li> 330 <li> 331 <a href="/policies/impressum">Impressum</a> 332 </li> 333 <li>
334 <span class="open-cookie-popup cursor-pointer">Cookie Einstellungen</span> 335 </li> 336 </ul> 337 </div> 338 </div> 339 </div> 340 </div> 341 </div> 342 </div> 343 344 <div class="bottom-footer"> 345 <div class="container-xxl"> 346 <div class="row flex-lg-row landing-page-content"> 347 <div class="col-auto d-flex align-items-center mb-2 mb-sm-0"> 348 <p>Entwickelt von Lemma GmbH</p> 349 </div> 350 <div class="col-auto d-flex justify-content-lg-end align-items-center ms-lg-auto"> 351 <p>Lemma GmbH | Ericusspitze 4 | D-20457 Hamburg</p> 352 </div> 353 </div> 354 </div> 355 </div> 356</footer> 357 <call-me></call-me> 358</div> 359 360 361 </div> 362 363 364
364<script> 365 window.COOKIE_DOMAIN = '.grammatip.de'; 366 window.isPWA = ('' === '1'); 367 </script>
367 368
368<script src="/assets/new/js/manifest.js?id=d4bcf00853dad0a3d96b3597ed8ec186"></script>
368 369
369<script src="/assets/new/js/vendor.js?id=a19c435f8194eff597d954c4ab255344"></script>
369 370
370<script src="/assets/new/js/app.js?id=4377efb2389f3ca32b5da6ebbf1fa628"></script>
370 371
371<script> 372 setInterval(function () { 373 var xmlHttp = new XMLHttpRequest(); 374 xmlHttp.open('GET', '/api/v1/keep-alive?time=' + Date.now(), true); 375 xmlHttp.send(null); 376 }, 300 * 1000); 377</script>
377 378
378<script> 379 // Sets the timeout variable to use later 380 let timeoutId; 381 382 // Listen for the window: online event 383 window.addEventListener('online', async (e) => { 384 timeoutId = setTimeout(loop, 2000); 385 }); 386 387 // Loop function to be called every 2 seconds, via setTimeout 388 async function loop() { 389 // "Ping" the server and wait for response. 390 let status = await ping() 391 if (status === 200) { 392 // Make new notification 393 let notification = { 394 detail: {message: "Internetverbindung ist wieder hergestellt.", type: 'success'}, 395 }; 396 397 // Launch the notification 398 const event = new CustomEvent('add-notification', notification); 399 window.dispatchEvent(event); 400 // Stops the looping 401 clearTimeout(timeoutId) 402 } else { 403 // Run the loop if the "ping" failed 404 timeoutId = setTimeout(loop, 2000); 405 } 406 } 407 408 // "ping" the current website 409 function ping() { 410 let URL = window.location.origin; 411 // Defines the configurations for the request 412 let settings = { 413 url: URL, 414 method: "HEAD", 415 timeout: 5000, 416 // Minimizing the data send 417 maxContentLength: 1, 418 maxBodyLength: 1, 419 transformResponse: [function (data, headers) { 420 // Return empty string, because we are not interested in any data 421 return ""; 422 }], 423 headers: { 424 accept: "application/json", 425 }, 426 }; 427 428 // Sends the request and the returned data is set to only be the status code 429 // Also catches and logs any error that might occur 430 return axios(settings) 431 .then((response) => this.status = response.status) 432 .catch((error) => console.log(error.message)); 433 } 434 435 436 // Listens to offline event and sends a notification 437 window.addEventListener('offline', (e) => { 438 clearTimeout(timeoutId) 439 let notification = { 440 detail: {message: "Es besteht momentan keine Internetverbindung.", type: 'error'}, 441 }; 442 443 const event = new CustomEvent('add-notification', notification); 444 445 window.dispatchEvent(event); 446 }); 447 448</script>
448 449 450 451 <!-- Matomo --> 452
452<script type="text/javascript"> 453 var consentCookie = JSON.parse(getCookieValue('COOKIE_CONSENT')); 454 var siteId = '4'; // Prod site id 455 456 var _paq = window._paq = window._paq || []; 457 if (!consentCookie || consentCookie.statistics != true) { 458 _paq.push(['requireCookieConsent']); // Require cookie consent before adding cookies 459 } 460 461 /* tracker methods like "setCustomDimension" should be called before "trackPageView" */ 462 463 464 _paq.push(['trackPageView']); 465 _paq.push(['enableLinkTracking']); 466 _paq.push(['enableHeartBeatTimer', 30]); 467 (function () { 468 var u = "//analytics.grammatip.com/"; 469 _paq.push(['setTrackerUrl', u + 'matomo.php']); 470 _paq.push(['setSiteId', siteId]); 471 var d = document, g = d.createElement('script'), s = d.getElementsByTagName('script')[0]; 472 g.type = 'text/javascript'; 473 g.async = true; 474 g.src = u + 'matomo.js'; 475 s.parentNode.insertBefore(g, s); 476 })(); 477 </script>
477 478 <!-- End Matomo Code --> 479 480 481
481<script> 482 // enable (bootstrap) tooltips 483 if (!window.isTouchDevice) { 484 var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]')) 485 var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) { 486 return new bootstrap.Tooltip(tooltipTriggerEl); 487 }) 488 } 489 </script>
489 490 491</body> 492</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.