PageSourceSearch

https://wiesing.de/feedback.html?lang=en

html wiesing.de collected 2026-10-09 23:32:33 UTC 37,301 bytes, 1,164 lines download raw bytes

1<!DOCTYPE html>
2<html lang="en" data-site-language="en" data-site-rendered="1">
3  <head>
4    <meta charset="UTF-8">
5    <meta name="viewport" content="width=device-width, initial-scale=1.0">
6    <title>Feedback zu den Apps | wiesing.de</title>
7    <meta name="description" content="Teilen Sie Ihr Feedback zu den Apps von Holger Wiesing mit: schnell, direkt und mit Mensch-Verifikation.">
8    <meta name="robots" content="index,follow">
9    <link rel="canonical" href="https://wiesing.de/feedback.html">
10    <meta property="og:locale" content="de_DE">
11    <meta property="og:type" content="website">
12    <meta property="og:site_name" content="wiesing.de">
13    <meta property="og:title" content="Feedback zu den Apps | wiesing.de">
14    <meta property="og:description" content="Teilen Sie Ihr Feedback zu den Apps von Holger Wiesing mit: schnell, direkt und mit Mensch-Verifikation.">
15    <meta property="og:url" content="https://wiesing.de/feedback.html">
16    <meta property="og:image" content="https://wiesing.de/assets/icons_logos/kapieren-de-logo-app-orange.png">
17    <meta name="twitter:card" content="summary_large_image">
18    <meta name="twitter:title" content="Feedback zu den Apps | wiesing.de">
19    <meta name="twitter:description" content="Teilen Sie Ihr Feedback zu den Apps von Holger Wiesing mit: schnell, direkt und mit Mensch-Verifikation.">
20    <meta name="twitter:image" content="https://wiesing.de/assets/icons_logos/kapieren-de-logo-app-orange.png">
21    <link rel="stylesheet" href="./assets/css/ui-shared.css?v=20260729-language6">
22    <link rel="icon" type="image/svg+xml" href="./assets/favicon.svg">
23    <link rel="shortcut icon" href="./assets/favicon.svg">
24    
24<script type="application/ld+json">
25      {
26        "@context": "https://schema.org",
27        "@type": "ContactPage",
28        "@id": "https://wiesing.de/feedback.html#webpage",
29        "url": "https://wiesing.de/feedback.html",
30        "name": "Feedback zu den Apps | wiesing.de",
31        "description": "Teilen Sie Ihr Feedback zu den Apps von Holger Wiesing mit: schnell, direkt und mit Mensch-Verifikation.",
32        "isPartOf": {
33          "@type": "WebSite",
34          "@id": "https://wiesing.de/#website",
35          "url": "https://wiesing.de/",
36          "name": "wiesing.de"
37        },
38        "inLanguage": "de"
39      }
40    </script>
40
41    
41<script src="https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit" async defer></script>
41
42    
42<script>
43      (function enforceCanonicalUrl() {
44        var canonicalHost = "wiesing.de";
45        var hostname = (window.location.hostname || "").toLowerCase();
46        var protocol = window.location.protocol;
47        var isLocal =
48          hostname === "localhost" ||
49          hostname === "127.0.0.1" ||
50          hostname === "[::1]";
51
52        if (isLocal || protocol === "file:") {
53          return;
54        }
55
56        var needsHttps = protocol !== "https:";
57        var needsCanonicalHost = hostname !== canonicalHost;
58        if (!needsHttps && !needsCanonicalHost) {
59          return;
60        }
61
62        var target =
63          "https://" +
64          canonicalHost +
65          window.location.pathname +
66          window.location.search +
67          window.location.hash;
68        window.location.replace(target);
69      })();
70    </script>
70
71    <style>
72      :root {
73        --bg: #ffffff;
74        --surface: #f8f7f4;
75        --surface-soft: #f2efe8;
76        --text: #2f2b25;
77        --text-soft: #5f5a52;
78        --border: #c8c2b6;
79        --accent: #7a3618;
80        --button: #0d6a48;
81        --button-strong: #084c34;
82        --shadow: 0 8px 17px rgba(52, 45, 36, 0.24);
83      }
84
85      * {
86        box-sizing: border-box;
87      }
88
89      html,
90      body {
91        height: 100%;
92        margin: 0;
93        padding: 0;
94      }
95
96      body {
97        min-height: 100vh;
98        min-height: 100svh;
99        min-height: 100dvh;
100        display: flex;
101        flex-direction: column;
102        font-family: "Avenir Next", "Segoe UI", "Helvetica Neue", sans-serif;
103        color: var(--text);
104        background: var(--bg);
105      }
106
107      .page {
108        width: min(920px, calc(100% - 2rem));
109        margin: 2.2rem auto 1.2rem;
110        flex: 1 0 auto;
111      }
112
113      .panel {
114        position: relative;
115        background: linear-gradient(160deg, #fbfaf7 0%, #f6f4ef 68%, #f2efe8 100%);
116        border: 1px solid var(--border);
117        box-shadow: var(--shadow);
118        border-radius: 12px;
119        padding: clamp(1.15rem, 3.2vw, 2rem);
120        padding-top: clamp(2.9rem, 5vw, 3.4rem);
121      }
122
123      h1 {
124        margin: 0.24rem 0 0.78rem;
125        font-size: clamp(1.54rem, 3.8vw, 2.2rem);
126        line-height: 1.2;
127      }
128
129      .panel-home-link {
130        position: absolute;
131        top: 0.85rem;
132        right: 0.95rem;
133        text-decoration: none;
134        color: #6e3620;
135        font-size: 0.88rem;
136        font-weight: 700;
137      }
138
139      .panel-home-link:hover,
140      .panel-home-link:focus-visible {
141        color: #5c2d1a;
142        text-decoration: underline;
143        text-underline-offset: 0.12em;
144        outline: none;
145      }
146
147      .intro {
148        margin: 0;
149        color: var(--text-soft);
150        line-height: 1.6;
151        max-width: 72ch;
152      }
153
154      .app-preview {
155        margin: 0 0 0.95rem;
156      }
157
158      .app-preview-title {
159        margin: 0 0 0.52rem;
160        font-size: 0.86rem;
161        letter-spacing: 0.08em;
162        text-transform: uppercase;
163        color: var(--accent);
164        font-weight: 800;
165      }
166
167      .app-strip {
168        display: flex;
169        flex-wrap: wrap;
170        gap: 0.56rem;
171      }
172
173      .app-chip {
174        display: inline-flex;
175        align-items: center;
176        gap: 0.45rem;
177        border: 1px solid #d1c9bc;
178        border-radius: 999px;
179        background: #fcfbf9;
180        text-decoration: none;
181        color: var(--text);
182        padding: 0.28rem 0.56rem 0.28rem 0.34rem;
183      }
184
185      .app-chip:hover,
186      .app-chip:focus-visible {
187        border-color: #b7ab9b;
188        box-shadow: 0 2px 5px rgba(70, 53, 38, 0.24);
189        outline: none;
190      }
191
192      .app-chip-icon {
193        width: 28px;
194        height: 28px;
195        object-fit: cover;
196        border-radius: 7px;
197        border: 1px solid #d4cec2;
198        background: #ece9e2;
199        flex-shrink: 0;
200      }
201
202      .app-chip-name {
203        font-size: 0.88rem;
204        font-weight: 600;
205      }
206
207      .app-strip-status {
208        margin: 0.48rem 0 0;
209        color: var(--text-soft);
210        font-size: 0.86rem;
211      }
212
213      .feedback-form {
214        margin-top: 0;
215        transition: opacity 0.22s ease, filter 0.22s ease;
216      }
217
218      .feedback-form-stage {
219        position: relative;
220        margin-top: 1.2rem;
221      }
222
223      .feedback-form-stage.is-submitted .feedback-form {
224        opacity: 0.26;
225        filter: grayscale(1);
226        pointer-events: none;
227        user-select: none;
228      }
229
230      .feedback-success-overlay {
231        position: absolute;
232        inset: 0;
233        display: flex;
234        align-items: center;
235        justify-content: center;
236        padding: 1rem;
237        opacity: 0;
238        pointer-events: none;
239        transition: opacity 0.22s ease;
240      }
241
242      .feedback-form-stage.is-submitted .feedback-success-overlay {
243        opacity: 1;
244        pointer-events: auto;
245      }
246
247      .feedback-success-card {
248        width: min(620px, calc(100% - 0.6rem));
249        background: rgba(255, 253, 248, 0.96);
250        border: 1px solid #d7cec0;
251        border-radius: 12px;
252        box-shadow: 0 9px 18px rgba(49, 38, 28, 0.28);
253        padding: clamp(1rem, 2.8vw, 1.35rem);
254        text-align: center;
255      }
256
257      .feedback-success-title {
258        margin: 0;
259        color: #14543b;
260        font-size: 1.06rem;
261        font-weight: 800;
262      }
263
264      .feedback-success-text {
265        margin: 0.5rem 0 0;
266        color: var(--text-soft);
267        line-height: 1.55;
268      }
269
270      .feedback-success-action {
271        margin-top: 0.84rem;
272      }
273
274      .feedback-success-link {
275        display: inline-flex;
276        align-items: center;
277        justify-content: center;
278        text-decoration: none;
279        border: 1px solid #0a5238;
280        background: linear-gradient(135deg, #0d6a48 0%, #0a5238 100%);
281        color: #ffffff;
282        font-weight: 700;
283        padding: 0.56rem 1.02rem;
284        border-radius: 9px;
285        box-shadow: 0 3px 7px rgba(10, 82, 56, 0.34);
286      }
287
288      .feedback-success-link:hover,
289      .feedback-success-link:focus-visible {
290        filter: brightness(1.06);
291        box-shadow: 0 4px 9px rgba(10, 82, 56, 0.42);
292        outline: none;
293      }
294
295      .form-grid {
296        display: grid;
297        grid-template-columns: repeat(2, minmax(0, 1fr));
298        gap: 0.9rem;
299      }
300
301      .field {
302        display: flex;
303        flex-direction: column;
304        gap: 0.35rem;
305      }
306
307      .field-full {
308        grid-column: 1 / -1;
309      }
310
311      label {
312        font-weight: 600;
313        font-size: 0.95rem;
314      }
315
316      .label-optional {
317        color: var(--text-soft);
318        font-weight: 500;
319      }
320
321      input,
322      select,
323      textarea {
324        width: 100%;
325        border: 1px solid #bdb6aa;
326        border-radius: 8px;
327        padding: 0.62rem 0.7rem;
328        background: #fffefb;
329        color: var(--text);
330        font: inherit;
331      }
332
333      textarea {
334        min-height: 150px;
335        resize: vertical;
336      }
337
338      input:focus-visible,
339      select:focus-visible,
340      textarea:focus-visible {
341        outline: 2px solid #21654a;
342        outline-offset: 1px;
343      }
344
345      .checkbox-wrap {
346        margin-top: 0.25rem;
347        display: flex;
348        flex-direction: column;
349        gap: 0.6rem;
350      }
351
352      .checkbox-row {
353        display: flex;
354        align-items: flex-start;
355        gap: 0.55rem;
356        background: var(--surface-soft);
357        border: 1px solid #d5cec2;
358        border-radius: 9px;
359        padding: 0.75rem 0.85rem;
360      }
361
362      .checkbox-row input[type="checkbox"] {
363        width: 1.08rem;
364        height: 1.08rem;
365        margin-top: 0.12rem;
366        flex-shrink: 0;
367      }
368
369      .checkbox-row label {
370        margin: 0;
371        font-weight: 500;
372        line-height: 1.5;
373      }
374
375      .captcha-title {
376        margin: 0 0 0.35rem;
377        font-size: 0.95rem;
378        font-weight: 700;
379        color: var(--accent);
380      }
381
382      .captcha-hint {
383        margin: 0 0 0.7rem;
384        color: var(--text-soft);
385        font-size: 0.9rem;
386      }
387
388      .turnstile-wrap {
389        background: var(--surface-soft);
390        border: 1px solid #d5cec2;
391        border-radius: 9px;
392        padding: 0.75rem 0.85rem;
393      }
394
395      .captcha-error {
396        margin: 0.55rem 0 0;
397        color: #9d2a1b;
398        font-weight: 700;
399        font-size: 0.9rem;
400      }
401
402      .actions {
403        margin-top: 1rem;
404        display: flex;
405        flex-wrap: wrap;
406        gap: 0.8rem;
407        align-items: center;
408      }
409
410      .submit-button {
411        border: 1px solid #0a5238;
412        background: linear-gradient(135deg, #0d6a48 0%, #0a5238 100%);
413        color: #ffffff;
414        font-weight: 700;
415        font-size: 0.98rem;
416        padding: 0.62rem 1.12rem;
417        border-radius: 9px;
418        cursor: pointer;
419        box-shadow: 0 3px 7px rgba(10, 82, 56, 0.34);
420      }
421
422      .submit-button:hover,
423      .submit-button:focus-visible {
424        filter: brightness(1.06);
425        box-shadow: 0 4px 9px rgba(10, 82, 56, 0.42);
426      }
427
428      .submit-button:focus-visible {
429        outline: 2px solid #0a5238;
430        outline-offset: 2px;
431      }
432
433      .error-message {
434        margin: 0;
435        color: #9d2a1b;
436        font-weight: 700;
437      }
438
439      .spam-trap {
440        position: absolute;
441        left: -9999px;
442        width: 1px;
443        height: 1px;
444        overflow: hidden;
445      }
446
447      .site-footer {
448        margin-top: auto;
449        flex-shrink: 0;
450        background: #3c3c3c;
451        border-top: 1px solid #2a2a2a;
452      }
453
454      .site-footer-inner {
455        width: min(920px, calc(100% - 2rem));
456        margin: 0 auto;
457        padding: 1rem 0;
458        display: flex;
459        justify-content: flex-end;
460        gap: 0.7rem;
461        flex-wrap: wrap;
462      }
463
464      .site-footer-link {
465        color: #f4f4f4;
466        text-decoration: none;
467        font-size: 0.92rem;
468        font-weight: 600;
469        padding: 0.45rem 0.7rem;
470        border: 1px solid #8a8a8a;
471        border-radius: 6px;
472      }
473
474      .site-footer-link.mediafiles-link {
475        min-width: 180px;
476        text-align: center;
477        background: #0d6a48;
478        border-color: #0b5a3d;
479        margin-right: auto;
480      }
481
482      .site-footer-link:hover,
483      .site-footer-link:focus-visible {
484        color: #ffffff;
485        border-color: #c5c5c5;
486      }
487
488      .site-footer-link.mediafiles-link:hover,
489      .site-footer-link.mediafiles-link:focus-visible {
490        border-color: #16734f;
491        background: #0f7751;
492      }
493
494      @media (max-width: 700px) {
495        .page {
496          width: min(920px, calc(100% - 1.1rem));
497          margin-top: 1.2rem;
498        }
499
500        .site-footer-inner {
501          width: min(920px, calc(100% - 1.1rem));
502        }
503
504        .form-grid {
505          grid-template-columns: 1fr;
506          gap: 0.8rem;
507        }
508
509        .app-strip {
510          gap: 0.44rem;
511        }
512
513        .app-chip {
514          padding-right: 0.5rem;
515        }
516
517        .panel {
518          padding-top: 3.2rem;
519        }
520
521        .panel-home-link {
522          top: 0.74rem;
523          right: 0.72rem;
524          font-size: 0.81rem;
525        }
526      }
527    </style>
528  </head>
529  <body>
530    <main class="page">
531      <section class="panel title-accent">
532        <a class="panel-home-link" href="./index.html?lang=en" aria-label="Back to home">Holger Wiesing &middot; kapieren.de</a>
533        <section class="app-preview" aria-labelledby="app-preview-title">
534          <p id="app-preview-title" class="app-preview-title">Current apps</p>
535          <div id="app-strip" class="app-strip" role="list" aria-label="Aktuelle Apps aus dem Katalog"></div>
536          <p id="app-strip-status" class="app-strip-status" hidden></p>
537        </section>
538
539        <h1>Your feedback on our apps</h1>
540        <p class="intro">
541          Your feedback and suggestions reach us directly and help us make the apps even better.
542        </p>
543
544        <div id="feedback-form-stage" class="feedback-form-stage">
545          <form id="feedback-form" class="feedback-form" novalidate>
546            <div class="form-grid">
547              <div class="field">
548                <label for="vorname">First name *</label>
549                <input id="vorname" name="vorname" type="text" autocomplete="given-name" required>
550              </div>
551
552              <div class="field">
553                <label for="name">Last name *</label>
554                <input id="name" name="name" type="text" autocomplete="family-name" required>
555              </div>
556
557              <div class="field">
558                <label for="email">E-Mail *</label>
559                <input id="email" name="email" type="email" autocomplete="email" required>
560              </div>
561
562              <div class="field">
563                <label for="schule">School <span class="label-optional">(optional)</span></label>
564                <input id="schule" name="schule" type="text" autocomplete="organization">
565              </div>
566
567              <div class="field">
568                <label for="funktion">Role at school <span class="label-optional">(optional)</span></label>
569                <input id="funktion" name="funktion" type="text" autocomplete="organization-title">
570              </div>
571
572              <div class="field">
573                <label for="app">Which app is this about? *</label>
574                <select id="app" name="app" required>
575                  <option value="">Loading apps ...</option>
576                </select>
577              </div>
578
579              <div class="field">
580                <label for="plattform">Which platform did you use the app on? *</label>
581                <select id="plattform" name="plattform" required>
582                  <option value="">Please select</option>
583                  <option value="iPhone">iPhone</option>
584                  <option value="iPad">iPad</option>
585                  <option value="Mac">Mac</option>
586                  <option value="Windows">Windows</option>
587                </select>
588              </div>
589
590              <div class="field field-full">
591                <label for="feedback-text">Your feedback *</label>
592                <textarea id="feedback-text" name="feedback_text" required></textarea>
593              </div>
594            </div>
595
596            <div class="checkbox-wrap">
597              <div class="checkbox-row">
598                <input id="newsletter-optin" name="newsletter_optin" type="checkbox">
599                <label for="newsletter-optin">
600                  Please email me about future features and other news about these apps.
601                </label>
602              </div>
603
604              <div>
605                <p class="captcha-title">Human verification (CAPTCHA)</p>
606                <p class="captcha-hint">Please complete the security check.</p>
607                <div class="turnstile-wrap">
608                  <div id="turnstile-container"></div>
609                </div>
610                <p id="captcha-error" class="captcha-error" role="alert" hidden>
611                  Please complete the CAPTCHA verification.
612                </p>
613              </div>
614            </div>
615
616            <div class="spam-trap" aria-hidden="true">
617              <label for="website">Website</label>
618              <input id="website" name="website" type="text" tabindex="-1" autocomplete="off">
619            </div>
620
621            <div class="actions">
622              <button class="submit-button" type="submit">Submit</button>
623              <p id="error-message" class="error-message" role="alert" hidden>
624                Something went wrong while sending. Please try again.
625              </p>
626            </div>
627          </form>
628
629          <div id="feedback-success-overlay" class="feedback-success-overlay" hidden>
630            <div class="feedback-success-card">
631              <p class="feedback-success-title">Thank you very much for your feedback.</p>
632              <p class="feedback-success-text">
633                Your feedback has been received successfully.
634              </p>
635              <p class="feedback-success-action">
636                <a class="feedback-success-link" href="./index.html?lang=en">Back to home</a>
637              </p>
638            </div>
639          </div>
640        </div>
641      </section>
642    </main>
643
644    <footer class="site-footer">
645      <div class="site-footer-inner">
646        <a class="site-footer-link mediafiles-link" href="./mediafiles/__auth/login.html?next=/mediafiles/">Media files (internal)</a>
647        <a class="site-footer-link" href="./index.html?lang=en">Home</a>
648        <a class="site-footer-link" href="./impressum.html?lang=en">Legal notice</a>
649      </div>
650    </footer>
651
652    
652<script src="./assets/js/site-i18n.js?v=20260909-stats1"></script>
652
653    
653<script>
654      (function initFeedbackForm() {
655        var defaultLocale = window.siteI18n && window.siteI18n.getLanguage ? window.siteI18n.getLanguage() : "de";
656        var tr = window.siteI18n && window.siteI18n.t ? window.siteI18n.t : function (value) { return value; };
657        var defaultSiteKey = "0x4AAAAAAC0_Q7Kezk3bRSYR";
658        var defaultCatalogConfig = {
659          country: "de",
660          storefronts: ["de", "us"],
661          language: "de-de",
662          developerName: "Holger Wiesing",
663          artistId: null
664        };
665        var captchaToken = "";
666        var turnstileWidgetId = null;
667        var turnstileReadyPromise = null;
668        var appStrip = document.getElementById("app-strip");
669        var appStripStatus = document.getElementById("app-strip-status");
670        var appSelect = document.getElementById("app");
671        var formStage = document.getElementById("feedback-form-stage");
672        var successOverlay = document.getElementById("feedback-success-overlay");
673
674        var form = document.getElementById("feedback-form");
675        var error = document.getElementById("error-message");
676        var captchaError = document.getElementById("captcha-error");
677        var submitButton = form ? form.querySelector('button[type="submit"]') : null;
678        var extraFeedbackApps = ["Vektoris3D 2.5 PLUS (PC/Mac)"];
679        if (!form || !error || !captchaError || !submitButton || !formStage || !successOverlay || !appSelect) {
680          return;
681        }
682
683        function setPending(isPending) {
684          submitButton.disabled = isPending;
685          submitButton.textContent = tr(isPending ? "Wird gesendet..." : "Absenden");
686        }
687
688        function setError(message) {
689          error.textContent = message;
690          error.hidden = false;
691        }
692
693        function setAppStripStatus(message) {
694          if (!appStripStatus) {
695            return;
696          }
697          appStripStatus.textContent = message;
698          appStripStatus.hidden = !message;
699        }
700
701        function waitForTurnstile() {
702          if (window.turnstile && typeof window.turnstile.render === "function") {
703            return Promise.resolve();
704          }
705          if (turnstileReadyPromise) {
706            return turnstileReadyPromise;
707          }
708
709          turnstileReadyPromise = new Promise(function (resolve, reject) {
710            var checks = 0;
711            var maxChecks = 80;
712            var intervalId = window.setInterval(function () {
713              checks += 1;
714              if (window.turnstile && typeof window.turnstile.render === "function") {
715                window.clearInterval(intervalId);
716                resolve();
717                return;
718              }
719              if (checks >= maxChecks) {
720                window.clearInterval(intervalId);
721                reject(new Error("Turnstile konnte nicht geladen werden."));
722              }
723            }, 100);
724          });
725
726          return turnstileReadyPromise;
727        }
728
729        function resetCaptcha() {
730          captchaToken = "";
731          if (window.turnstile && typeof window.turnstile.reset === "function") {
732            if (turnstileWidgetId !== null) {
733              window.turnstile.reset(turnstileWidgetId);
734            } else {
735              window.turnstile.reset();
736            }
737          }
738        }
739
740        function initTurnstileWidget(siteKey) {
741          return waitForTurnstile().then(function () {
742            var container = document.getElementById("turnstile-container");
743            if (!container) {
744              return;
745            }
746
747            turnstileWidgetId = window.turnstile.render(container, {
748              sitekey: siteKey || defaultSiteKey,
749              language: "de",
750              theme: "light",
751              callback: function (token) {
752                captchaToken = String(token || "");
753                captchaError.hidden = true;
754              },
755              "expired-callback": function () {
756                captchaToken = "";
757              },
758              "error-callback": function () {
759                captchaToken = "";
760              }
761            });
762          });
763        }
764
765        function loadFeedbackConfig() {
766          return fetch("/api/feedback-config.php", { cache: "no-store" })
767            .then(function (response) {
768              if (!response.ok) {
769                throw new Error("Konfiguration konnte nicht geladen werden.");
770              }
771              return response.json();
772            })
773            .catch(function () {
774              return { siteKey: defaultSiteKey };
775            });
776        }
777
778        function normalize(value) {
779          return String(value || "")
780            .trim()
781            .toLowerCase();
782        }
783
784        function buildItunesUrl(path, params) {
785          return "https://itunes.apple.com" + path + "?" + new URLSearchParams(params).toString();
786        }
787
788        function getStorefronts(config) {
789          var all = [config.country].concat(config.storefronts || [])
790            .map(function (entry) {
791              return normalize(entry);
792            })
793            .filter(function (entry) {
794              return /^[a-z]{2}$/.test(entry);
795            });
796          return Array.from(new Set(all));
797        }
798
799        function isSoftwareResult(item) {
800          return Boolean(item) && (item.wrapperType === "software" || item.kind === "software");
801        }
802
803        function matchesDeveloper(app, config) {
804          var needle = normalize(config.developerName);
805          if (!needle) {
806            return true;
807          }
808          var tokens = needle.split(/\s+/).filter(Boolean);
809          var artist = normalize(app.artistName);
810          var seller = normalize(app.sellerName);
811          var haystack = (artist + " " + seller).trim();
812
813          return (
814            artist === needle ||
815            seller === needle ||
816            artist.indexOf(needle) >= 0 ||
817            seller.indexOf(needle) >= 0 ||
818            tokens.every(function (token) {
819              return haystack.indexOf(token) >= 0;
820            })
821          );
822        }
823
824        function byRecentRelease(a, b) {
825          var dateA = new Date(a.currentVersionReleaseDate || a.releaseDate || 0).getTime();
826          var dateB = new Date(b.currentVersionReleaseDate || b.releaseDate || 0).getTime();
827          return dateB - dateA;
828        }
829
830        function uniqueByTrackId(items) {
831          var byId = new Map();
832          items.forEach(function (item) {
833            if (item.trackId && !byId.has(item.trackId)) {
834              byId.set(item.trackId, item);
835            }
836          });
837          return Array.from(byId.values());
838        }
839
840        function fetchJsonp(url) {
841          return new Promise(function (resolve, reject) {
842            var callbackName = "itunesCallback_" + Date.now() + "_" + Math.random().toString(36).slice(2, 9);
843            var script = document.createElement("script");
844            var timeout = window.setTimeout(function () {
845              cleanup();
846              reject(new Error("Timeout while loading JSONP"));
847            }, 10000);
848
849            function cleanup() {
850              window.clearTimeout(timeout);
851              if (script.parentNode) {
852                script.parentNode.removeChild(script);
853              }
854              delete window[callbackName];
855            }
856
857            window[callbackName] = function (data) {
858              cleanup();
859              resolve(data);
860            };
861
862            script.onerror = function () {
863              cleanup();
864              reject(new Error("JSONP request failed"));
865            };
866            script.src = url + (url.indexOf("?") >= 0 ? "&" : "?") + "callback=" + callbackName;
867            document.head.appendChild(script);
868          });
869        }
870
871        async function fetchItunesData(url) {
872          try {
873            var response = await fetch(url, { cache: "no-store" });
874            if (!response.ok) {
875              throw new Error("HTTP " + response.status);
876            }
877            return response.json();
878          } catch (_err) {
879            return fetchJsonp(url);
880          }
881        }
882
883        async function discoverArtistIds(config) {
884          if (config.artistId) {
885            return [String(config.artistId)];
886          }
887          if (!config.developerName) {
888            return [];
889          }
890
891          var artistIds = new Set();
892          var storefronts = getStorefronts(config);
893          await Promise.all(storefronts.map(async function (storefront) {
894            var url = buildItunesUrl("/search", {
895              term: config.developerName,
896              entity: "software",
897              attribute: "softwareDeveloper",
898              country: storefront,
899              lang: config.language,
900              limit: "200"
901            });
902            try {
903              var payload = await fetchItunesData(url);
904              (payload.results || []).forEach(function (item) {
905                if (isSoftwareResult(item) && item.artistId && matchesDeveloper(item, config)) {
906                  artistIds.add(String(item.artistId));
907                }
908              });
909            } catch (_searchError) {
910              // Ignore single-store lookup failures.
911            }
912          }));
913
914          return Array.from(artistIds.values());
915        }
916
917        async function loadAppsForArtistIds(artistIds, config) {
918          var storefronts = getStorefronts(config);
919          var requests = [];
920          artistIds.forEach(function (artistId) {
921            storefronts.forEach(function (storefront) {
922              var url = buildItunesUrl("/lookup", {
923                id: artistId,
924                entity: "software",
925                country: storefront,
926                lang: config.language,
927                limit: "200"
928              });
929              requests.push(fetchItunesData(url).catch(function () {
930                return null;
931              }));
932            });
933          });
934
935          var payloads = await Promise.all(requests);
936          var allApps = [];
937          payloads.forEach(function (payload) {
938            (payload && payload.results ? payload.results : []).forEac
938h(function (item) {
939              if (isSoftwareResult(item)) {
940                allApps.push(item);
941              }
942            });
943          });
944          return allApps;
945        }
946
947        async function fallbackSearchApps(config) {
948          if (!config.developerName) {
949            return [];
950          }
951          var storefronts = getStorefronts(config);
952          var payloads = await Promise.all(
953            storefronts.map(async function (storefront) {
954              var url = buildItunesUrl("/search", {
955                term: config.developerName,
956                entity: "software",
957                country: storefront,
958                lang: config.language,
959                limit: "200"
960              });
961              try {
962                return await fetchItunesData(url);
963              } catch (_err) {
964                return null;
965              }
966            })
967          );
968
969          var allApps = [];
970          payloads.forEach(function (payload) {
971            (payload && payload.results ? payload.results : []).forEach(function (item) {
972              if (isSoftwareResult(item) && matchesDeveloper(item, config)) {
973                allApps.push(item);
974              }
975            });
976          });
977          return allApps;
978        }
979
980        async function fetchCatalogApps(config) {
981          var artistIds = await discoverArtistIds(config);
982          var apps = [];
983          var loadedViaArtistLookup = false;
984          if (artistIds.length) {
985            apps = await loadAppsForArtistIds(artistIds, config);
986            loadedViaArtistLookup = apps.length > 0;
987          }
988          if (!apps.length) {
989            apps = await fallbackSearchApps(config);
990          }
991          var filtered = loadedViaArtistLookup || config.artistId
992            ? apps
993            : apps.filter(function (item) {
994              return matchesDeveloper(item, config);
995            });
996          return uniqueByTrackId(filtered).sort(byRecentRelease);
997        }
998
999        function createAppChip(app) {
1000          var chip = document.createElement("a");
1001          chip.className = "app-chip";
1002          chip.href = window.siteI18n.appStoreUrl(app);
1003          chip.referrerPolicy = "same-origin";
1004          chip.target = "_blank";
1005          chip.rel = "noopener";
1006          chip.setAttribute("role", "listitem");
1007
1008          var icon = document.createElement("img");
1009          icon.className = "app-chip-icon";
1010          icon.src = String(app.artworkUrl100 || app.artworkUrl60 || "");
1011          icon.alt = String((app.trackName || "App") + " Icon");
1012          icon.loading = "lazy";
1013
1014          var name = document.createElement("span");
1015          name.className = "app-chip-name";
1016          name.textContent = String(app.trackName || tr("Unbenannte App"));
1017
1018          chip.appendChild(icon);
1019          chip.appendChild(name);
1020          return chip;
1021        }
1022
1023        function populateAppSelect(apps) {
1024          appSelect.textContent = "";
1025
1026          var optionNames = [];
1027          apps.forEach(function (app) {
1028            var name = String(app.trackName || "").trim();
1029            if (name && optionNames.indexOf(name) === -1) {
1030              optionNames.push(name);
1031            }
1032          });
1033          extraFeedbackApps.forEach(function (name) {
1034            if (optionNames.indexOf(name) === -1) {
1035              optionNames.push(name);
1036            }
1037          });
1038
1039          var placeholder = document.createElement("option");
1040          placeholder.value = "";
1041          placeholder.textContent = tr(optionNames.length ? "Bitte App auswählen" : "Aktuell keine Apps verfügbar");
1042          appSelect.appendChild(placeholder);
1043
1044          optionNames.forEach(function (name) {
1045            var option = document.createElement("option");
1046            option.value = name;
1047            option.textContent = name;
1048            appSelect.appendChild(option);
1049          });
1050        }
1051
1052        async function loadAppPreview() {
1053          if (!appStrip) {
1054            return;
1055          }
1056          setAppStripStatus("Lade aktuelle Apps ...");
1057          try {
1058            var localeResponse = await fetch("./locales/" + defaultLocale + ".json", {
1059              cache: "no-store"
1060            });
1061            if (!localeResponse.ok) {
1062              throw new Error("Locale konnte nicht geladen werden.");
1063            }
1064            var locale = await localeResponse.json();
1065            var config = Object.assign({}, defaultCatalogConfig, locale && locale.catalogConfig ? locale.catalogConfig : {});
1066            if (!Array.isArray(config.storefronts)) {
1067              config.storefronts = defaultCatalogConfig.storefronts.slice();
1068            }
1069            var apps = await fetchCatalogApps(config);
1070            populateAppSelect(apps);
1071            var currentApps = apps.slice(0, 8);
1072            appStrip.textContent = "";
1073            currentApps.forEach(function (app) {
1074              appStrip.appendChild(createAppChip(app));
1075            });
1076            setAppStripStatus(currentApps.length ? "" : "Aktuell konnten keine Apps geladen werden.");
1077          } catch (_err) {
1078            populateAppSelect([]);
1079            appStrip.textContent = "";
1080            setAppStripStatus("Aktuell konnten keine Apps geladen werden.");
1081          }
1082        }
1083
1084        loadAppPreview();
1085
1086        loadFeedbackConfig()
1087          .then(function (config) {
1088            return initTurnstileWidget(config.siteKey);
1089          })
1090          .catch(function (initError) {
1091            setError(initError.message || "Captcha konnte nicht initialisiert werden.");
1092          });
1093
1094        form.addEventListener("submit", async function (event) {
1095          event.preventDefault();
1096          error.hidden = true;
1097          captchaError.hidden = true;
1098
1099          if (!form.reportValidity()) {
1100            return;
1101          }
1102
1103          var trap = document.getElementById("website");
1104          if (trap && String(trap.value || "").trim()) {
1105            return;
1106          }
1107
1108          if (!captchaToken) {
1109            captchaError.hidden = false;
1110            return;
1111          }
1112
1113          setPending(true);
1114          try {
1115            var payload = {
1116              vorname: String(form.vorname.value || "").trim(),
1117              name: String(form.name.value || "").trim(),
1118              email: String(form.email.value || "").trim(),
1119              schule: String(form.schule.value || "").trim(),
1120              funktion: String(form.funktion.value || "").trim(),
1121              app: String(form.app.value || "").trim(),
1122              plattform: String(form.plattform.value || "").trim(),
1123              feedbackText: String(form.feedback_text.value || "").trim(),
1124              newsletterOptIn: Boolean(form.newsletter_optin.checked),
1125              website: trap ? String(trap.value || "").trim() : "",
1126              turnstileToken: captchaToken
1127            };
1128
1129            var response = await fetch("/api/feedback.php", {
1130              method: "POST",
1131              headers: { "Content-Type": "application/json" },
1132              body: JSON.stringify(payload)
1133            });
1134            var result = await response.json().catch(function () {
1135              return {};
1136            });
1137
1138            if (!response.ok || !result.ok) {
1139              if (result.code === "captcha_invalid") {
1140                captchaError.textContent = tr("Captcha ungültig oder abgelaufen. Bitte erneut bestätigen.");
1141                captchaError.hidden = false;
1142              } else {
1143                setError(result.error || tr("Beim Senden ist ein Fehler aufgetreten."));
1144              }
1145              resetCaptcha();
1146              return;
1147            }
1148
1149            form.reset();
1150            resetCaptcha();
1151            captchaError.textContent = tr("Bitte führen Sie die Captcha-Verifikation durch.");
1152            formStage.classList.add("is-submitted");
1153            successOverlay.hidden = false;
1154          } catch (_err) {
1155            setError("Der Server ist momentan nicht erreichbar. Bitte später erneut versuchen.");
1156            resetCaptcha();
1157          } finally {
1158            setPending(false);
1159          }
1160        });
1161      })();
1162    </script>
1162
1163  </body>
1164</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.