A introspective Female From UK, studied fashion marketing in London in their 48, grandmother sharing joy and family recipes, wearing a futuristic space pilot plugsuit with glowing lines, tying a shoelace in a theater stage.
Photo generated by z-image-turbo (AI)

💡 Czemu ludzie szukają “onlyfans logo svg” i co tu znajdziesz

Wyszukiwanie “onlyfans logo svg” najczęściej robi ktoś, kto chce szybko wrzucić logo na banner, miniaturę, koszulkę albo do stopki wideo. Problem jest prosty: plik w wysokiej jakości, skalowalny i edytowalny (czyli SVG) oszczędza czas i wygląda profesjonalnie — ale wywołuje też pytania: skąd wziąć oficjalne logo, czy można je modyfikować, co z prawem autorskim i jak zoptymalizować SVG pod web?

W tym artykule dostajesz praktyczny plan: gdzie szukać (i czego unikać), jak konwertować i edytować logo bez utraty jakości, techniki optymalizacji i wbudowania SVG w stronę lub merch. Na dokładkę wrzucam kontekst społeczny — dlaczego użycie loga ma znaczenie w 2025 (twórcy, buzzy newsy i dyskusje o zarobkach) — i legalne tipy, które pozwolą ci spać spokojniej.

📊 Porównanie platform: kto oferuje najlepsze narzędzia brandingowe? (platform differences)

🧑‍🎤 Platforma💰 Opłaty (fee)📈 Narzędzia brandingowe🛠️ Pliki wektorowe
OnlyFans20%Szerokie — profile, paywalle, promoOficjalne zasoby ograniczone
Fansly15%Dobre szablony, tagiZazwyczaj brak SVG
Patreon5–12%Rozbudowane strony twórcówPliki SVG dla twórców łatwiejsze

Tabela pokazuje prosty fakt: OnlyFans ma mocne narzędzia monetyzacji i promocji, ale oficjalne wektorowe zasoby (SVG) bywają ograniczone lub trudno dostępne. To tłumaczy, dlaczego twórcy robią własne wersje logotypu — i dlaczego warto być ostrożnym z prawami autorskimi i zgodnością stylistyczną.

W praktyce oznacza to:

  • Jeśli potrzebujesz logotypu do niekomercyjnego użycia (np. artykuł, cytat), prosta, wierna wersja SVG z zachowaniem kolorów i kształtów zwykle wystarczy.
  • Dla merchu lub współpracy komercyjnej — dopilnuj zgody, bo użycie logo bez zgody to proszenie się o problem.
  • Nawet jeśli firma publikuje PNG, możesz samodzielnie przygotować SVG — ale zadbaj o jakość i metadane.

😎 MaTitie: CZAS NA POKAZ

Cześć — jestem MaTitie, autor tego wpisu i wielki fan sprytnych trików z internetu. Testowałem dziesiątki VPN-ów i wiem, że czasem trzeba ominąć blokady, by mieć dostęp do swoich platform. Jeśli chcesz prywatności i szybkiego dostępu do serwisów streamingowych czy platform dla twórców — mam prostą rekomendację.

Jeśli chcesz szybko, bez dramatu i z pełną prywatnością:
👉 🔐 Spróbuj NordVPN teraz — 30 dni zwrotu.

Działa dobrze w Polsce, ma szybkie serwery i łatwą konfigurację.

Ten link jest afiliacyjny — MaTitie może zarobić małą prowizję jeśli kupisz przez niego usługę.

💡 Gdzie znaleźć oryginalne pliki i jakie są alternatywy

Krótko i na temat:

  • Oficjalne zasoby brandu (press kit) — najlepsza opcja, jeśli OnlyFans udostępni. Nie zawsze publicznie dostępne.
  • Media kit od partnerów i agencji — czasem brand assets trafiają przez partnerów.
  • Wersje użytkowników i społeczności (GitHub, repo grafiki) — używaj ostrożnie.
  • Konwersja z wysokiej jakości PNG/JPEG do SVG przy pomocy Inkscape lub Adobe Illustrator — często jedyne wyjście, gdy nie ma oryginału.

Kontekst społeczny: dyskusje o tym, kto i jak korzysta z platform (i brandu) nadal są głośne. Publiczne wypowiedzi celebrytów i obrońców twórców podgrzewają debatę o wizerunku platformy — np. Bethenny Frankel broniła prawa twórców do decydowania o własnym ciele i działalności, co wpływa na to, jak marki i twórcy używają nazw i logotypów.[AOL, 2025-08-29]

Dodatkowo, kultura “hype” wokół konkretnych twórców (np. Bonnie Blue) podkreśla, że wykorzystanie marki w promocji może dać szum — ale też problemy, jeśli sugeruje oficjalne powiązania.[The Tab, 2025-08-29]

Wreszcie — biznesowa strona platformy: firma stojąca za OnlyFans raportowała rekordowe zyski, podczas gdy średnie zarobki modeli bywały niższe — to zmienia dynamikę tego, kto i jak używa brandu w marketingu.[Mirror, 2025-08-29]

🔧 Krok po kroku: jak stworzyć i zoptymalizować własne OnlyFans logo SVG (praktyczny przewodnik)

  1. Źródło obrazu: znajdź możliwie najlepsze PNG/JPEG — minimalne rozmycie, wysoki kontrast.
  2. Konwersja:
    • Inkscape: Import → Trace Bitmap → ustawienia detekcji krawędzi → Eksport do SVG.
    • Adobe Illustrator: Image Trace → Expand → Clean up → Save as SVG.
  3. Sprzątanie:
    • Usuń zbędne warstwy, metadane, komentarze.
    • Zmień nazwy id na krótsze, jeśli potrzebujesz mniejszych rozmiarów.
  4. Optymalizacja:
    • SVGO (CLI) lub pluginy (Webpack, Gulp) → usuń puste grupy, niepotrzebne atrybuty.
    • Skala: zamień px na względne unit’y lub viewBox, by SVG był responsywny.
  5. Dostępność:
    • Dodaji <desc> w pliku SVG, by screenreadery rozumiały logo.</li> </ul> </li> <li>Użycie: <ul> <li>Do strony: inline SVG (najlepsze do kontroli CSS) lub <img src="logo.svg"> (prostsze).</li> <li>Do social/miniatur: eksport PNG w potrzebnych rozmiarach (1x, 2x).</li> </ul> </li> <li>Prawa i oznaczenia: <ul> <li>Nie modyfikuj loga w sposób, który sugeruje, że marka popiera twój projekt.</li> <li>Dla merchu lub komercji — prośba o licencję to standard.</li> </ul> </li> </ol> <p>Praktyczny tip: zawsze trzymaj oryginalną wersję SVG i wersję zoptymalizowaną oddzielnie. Wersja robocza może mieć pełne id, komentarze i warstwy — ułatwia to późniejsze poprawki.</p> <h2 id="-najczęściej-zadawane-pytania">🙋 Najczęściej zadawane pytania<a hidden class="anchor" aria-hidden="true" href="#-najczęściej-zadawane-pytania">¶</a></h2> <p>❓ <strong>Czy mogę użyć logo OnlyFans na koszulce, którą sprzedaję?</strong></p> <p>💬 <em>Zwykle nie bez zgody. Przy sprzedaży merchu najlepiej uzyskać licencję lub pisemne pozwolenie od właściciela marki. Jeżeli używasz logo w kontekście krytyki lub opisu ujęcia dziennikarskiego, to może być chronione jako dozwolony użytek — ale lepiej to skonsultować.</em></p> <p>🛠️ <strong>Jak bezpiecznie osadzić SVG na stronie, żeby nie łamać prawa?</strong></p> <p>💬 <em>Upewnij się, że masz prawo do użycia pliku (licencja lub zasoby oficjalne). Jeśli to twoja grafika, dodaj w stopce informację o autorze i nie używaj logo w sposób sugerujący oficjalne partnerstwo. Technicznie — używaj inline SVG dla lepszej kontroli CSS i dodaj tekst alternatywny dla SEO i dostępności.</em></p> <p>🧠 <strong>Czy zmiana koloru lub dodanie hasła do loga to już naruszenie?</strong></p> <p>💬 <em>Zmiana estetyczna może w większym stopniu sugerować współpracę lub endorsement. Małe modyfikacje do użytku prywatnego zwykle są tolerowane, ale komercyjne wykorzystanie zmodyfikowanego loga bez zgody to ryzykowny krok.</em></p> <h2 id="-finalne-wnioski">🧩 Finalne wnioski<a hidden class="anchor" aria-hidden="true" href="#-finalne-wnioski">¶</a></h2> <p>Logo OnlyFans w formacie SVG to wygodne narzędzie w rękach twórców i projektantów — pod warunkiem, że używasz go świadomie. Technicznie SVG daje pełną kontrolę, oszczędza transfer i wygląda ostrzej niż PNG. Społeczny i prawny kontekst (głośne wypowiedzi i biznesowe wyniki platform) oznacza jednak, że każde publiczne użycie loga powinno być rozważne: prośba o zgodę, transparentność i szacunek do brandu to proste, ale skuteczne zasady.</p> <h2 id="-dalsze-lektury">📚 Dalsze lektury<a hidden class="anchor" aria-hidden="true" href="#-dalsze-lektury">¶</a></h2> <p>Oto kilka aktualnych artykułów z News Pool, które rozjaśniają kontekst rynku twórców i dyskusji wokół platform:</p> <p>🔸 <strong>Tennessee Cop Arrested and Charged For Groping OnlyFans Model During Fake Traffic Stop</strong><br> 🗞️ Source: Yahoo – 📅 2025-08-29<br> 🔗 <a href="https://www.yahoo.com/entertainment/celebrity/articles/tennessee-cop-arrested-charged-groping-160427257.html" rel="nofollow" target="_blank">Read Article</a></p> <p>🔸 <strong>AI Billionaire Lucy Guo Pushes Into Crowded Social Media Field</strong><br> 🗞️ Source: Mint – 📅 2025-08-29<br> 🔗 <a href="https://www.livemint.com/companies/news/ai-billionaire-lucy-guo-pushes-into-crowded-social-media-field-11756496391041.html" rel="nofollow" target="_blank">Read Article</a></p> <p>🔸 <strong>‘We’d sell our house for you’: Lily Phillips’ parents break down, beg her to quit OnlyFans</strong><br> 🗞️ Source: The Economic Times – 📅 2025-08-29<br> 🔗 <a href="https://economictimes.indiatimes.com/news/new-updates/wed-sell-our-house-for-you-lily-phillips-parents-break-down-beg-her-to-quit-onlyfans-say-what-wrong-did-we-do/articleshow/123581801.cms" rel="nofollow" target="_blank">Read Article</a></p> <h2 id="-mała-prosta-reklama--nic-wstydliwego-serio">😅 Mała prosta reklama — nic wstydliwego, serio<a hidden class="anchor" aria-hidden="true" href="#-mała-prosta-reklama--nic-wstydliwego-serio">¶</a></h2> <p>Jeśli tworzysz treści na OnlyFans, Fansly czy podobnych platformach i chcesz, żeby twoja praca nie przepadła w morzu contentu — sprawdź Top10Fans. To globalny hub, który wyrzuca twórców na widok publiczny według regionu i kategorii. Masz szansę dostać darmowy miesiąc promocji na stronie głównej.</p> <p>✅ Ranking regionalny i kategorie<br> ✅ Widoczność w 100+ krajach<br> 🎁 Oferta: 1 miesiąc bezpłatnej promocji po dołączeniu!</p> <section class="cta-folding-form" style="margin:3rem 0;padding:2rem;text-align:center;background:#fdfdfd;border-radius:12px; box-shadow:0 1px 4px rgba(0,0,0,.06);position:relative;"> <a class="ad-button" href="https://top10fans.world/join/?utm_welcome=top10fans.pl" target="_blank" rel="noopener noreferrer"> 👉 Dołącz za darmo <span class="live-dot-button" aria-hidden="true"></span> </a> <style> .cta-folding-form .ad-button{ display:inline-block; padding:10px 32px 10px 20px; background:#ff4081; color:#fff; border-radius:6px; text-decoration:none; font-weight:bold; font-size:1rem; transition:background .2s ease; position:relative; overflow:hidden; box-shadow:0 2px 6px rgba(0,0,0,0.1); } .cta-folding-form .ad-button:hover{ background:#e63770; transform:translateY(-2px); box-shadow:0 4px 10px rgba(0,0,0,0.15); } .cta-folding-form .live-dot-button{ position:absolute; top:4px; right:4px; width:5px;height:5px; border-radius:50%; background:#fff; box-shadow:0 0 4px 0 rgba(255,64,129,.7); animation:pulse-dot 1s infinite ease-in-out; pointer-events:none; } @keyframes pulse-dot{ 0%,100%{ transform:scale(1); box-shadow:0 0 4px 0 rgba(255,64,129,.5); opacity:.9; } 50%{ transform:scale(1.6); box-shadow:0 0 6px 0 rgba(255,64,129,.9); opacity:.45; } } .dark .cta-folding-form{ background:#1e1e1e; color:#ddd; } .dark .cta-folding-form .ad-button{ background:#ff4081; color:#fff; } .dark .cta-folding-form .ad-button:hover{ background:#e63770; } </style> </section> <h2 id="-zrzeczenie-się-odpowiedzialności">📌 Zrzeczenie się odpowiedzialności<a hidden class="anchor" aria-hidden="true" href="#-zrzeczenie-się-odpowiedzialności">¶</a></h2> <blockquote> <p>Ten wpis łączy publicznie dostępne informacje, obserwacje i trochę AI-pomocy. Nie wszystkie szczegóły mogą być oficjalnie potwierdzone — traktuj ten tekst jako praktyczny przewodnik, nie poradę prawną. W razie wątpliwości konsultuj się z prawnikiem lub bezpośrednio z właścicielem marki.</p></blockquote> </div> <div id="papermod-comment-root" data-post-id="12215acecf9953f4c16a9c27ff3ed3c2" data-post-title="OnlyFans logo SVG: How to Use, Edit & Stay Safe"> <div id="papermod-comment-panel" style=" width: 100%; max-width: 720px; margin: 2rem auto; background: #fff; border-radius: 12px; box-shadow: 0 4px 16px rgba(0,0,0,0.08); overflow: hidden; font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; "> <div style=" padding: 1rem 1.2rem; background: #ff4081; color: #fff; display:flex; align-items:center; justify-content:space-between; "> <div style="font-size:1rem; font-weight:600;">Post</div> <button id="papermod-clear-comment-cache" style=" background: transparent; border: none; color: #fff; cursor: pointer; font-size: .8rem; padding: .2rem .5rem; border-radius: 4px; transition: background 0.2s ease; "> Clear Draft </button> </div> <form id="papermod-comment-form" style="padding: 1rem 1.2rem; background:#fff;"> <textarea id="papermod-comment-input" rows="4" placeholder="Top10Fans korzysta z anonimowych komentarzy. Po przesłaniu treść może zostać przeredagowana przez AI (w tym filtrowanie informacji wrażliwych). Klikając „Post”, wyrażasz zgodę." style=" width:100%; resize:vertical; min-height: 100px; border-radius:8px; border:1px solid #ddd; padding:.7rem .8rem; font-size:.9rem; box-sizing:border-box; margin-bottom:.5rem; transition: border-color 0.2s ease; line-height: 1.5; "></textarea> <div id="papermod-comment-wordcount" style=" text-align: right; font-size: .8rem; color: #666; margin-bottom: 1rem; height: 1rem; "> <span id="comment-wordcount-number">0</span>/300 </div> <div id="papermod-comment-overlimit" style=" text-align: center; font-size: .8rem; color: #ff4444; margin-bottom: 1rem; display: none; "> Możesz wpisać maksymalnie 300 znaków. Usuń część treści i spróbuj ponownie. </div> <div id="papermod-sensitive-tip" style=" text-align: center; font-size: .8rem; color: #ff9900; margin-bottom: 1rem; display: none; "> Nie wysyłaj wrażliwych danych osobowych, takich jak numery dowodów osobistych, numery kart bankowych czy numery telefonów. </div> <div id="papermod-submit-status" style=" text-align: center; font-size: .8rem; margin-bottom: 1rem; display: none; "></div> <div style=" display:flex; justify-content:center; margin-top: 1rem; "> <button type="submit" id="papermod-comment-submit" style=" padding: 0.7rem 2rem; min-height: 48px; min-width: 140px; border-radius:999px; border:none; font-size: .9rem; cursor:pointer; background: #ff4081; /* 原#4285F4替换为#ff4081 */ color:#fff; display:flex; align-items:center; justify-content:center; box-shadow: 0 2px 6px rgba(0,0,0,0.08); transition: all 0.2s ease; "> <svg width="20" height="20" viewBox="0 0 24 24" aria-hidden="true" style=" display: block; height: 60%; width: auto; margin-right: 0.5rem; "> <path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z" fill="currentColor" /> </svg> <span>Post</span> </button> </div> </form> </div> </div> <style> #papermod-comment-submit:hover { background: #e63770; transform: scale(1.02); box-shadow: 0 3px 8px rgba(0,0,0,0.1); } #papermod-comment-submit:active { background: #d12f63; transform: scale(0.98); } #papermod-comment-submit:disabled { background: #ff80aa; cursor: not-allowed; transform: none; box-shadow: none; } #papermod-comment-input:focus { border-color: #ff4081; outline: none; box-shadow: 0 0 0 2px rgba(255, 64, 129, 0.1); } .comment-wordcount-overlimit { color: #ff4444 !important; font-weight: 500; } #papermod-comment-input.overlimit { border-color: #ff4444 !important; box-shadow: 0 0 0 2px rgba(255, 68, 68, 0.1) !important; } #papermod-comment-cache:hover { background: rgba(255,255,255,0.2); } @media (max-width: 767px) { #papermod-comment-panel { margin: 1rem; border-radius: 8px; } #papermod-comment-form { padding: .8rem; } #papermod-comment-input { font-size: .85rem; padding: .6rem .7rem; } } </style> <script> (function () { const CONFIG = { colors: { primary: "#4285F4", primaryHover: "#3367d6", primaryActive: "#2956b9", danger: "#ff4444", warning: "#ff9900", success: "#00c851", gray: "#666" }, api: { endpoint: "https://chat.top10fans.us/dingtalk-chat", timeout: 10000 }, limits: { maxCharacters: 300, rateLimit: 3, rateWindowMs: 5 * 60 * 1000 }, storage: { prefix: "papermod_comment_", expireMs: 7 * 24 * 60 * 60 * 1000 }, texts: { overLimit: "Możesz wpisać maksymalnie {max} znaków. Usuń część treści i spróbuj ponownie.", sensitiveTip: "Nie wysyłaj wrażliwych danych osobowych, takich jak numery dowodów osobistych, numery kart bankowych czy numery telefonów.", repeatSubmit: "Nie wysyłaj wielokrotnie. Spróbuj ponownie później.", rateLimitTip: "Zbyt często wysyłasz komentarze. Spróbuj ponownie za 5 minut.", sendSuccess: "Komentarz został pomyślnie wysłany! Sprawdzimy go i wyświetlimy tak szybko, jak to możliwe.", sendFailed: "Nie udało się wysłać komentarza. Spróbuj ponownie później.", clearConfirm: "Czy na pewno chcesz usunąć szkic komentarza?", clearSuccess: "Szkic komentarza został usunięty.", sensitiveReject: "Komentarz zawiera wrażliwe informacje. Zmień go i wyślij ponownie.", emptyComment: "Wpisz treść komentarza przed wysłaniem.", successCountdown: "Będziesz mógł skomentować ponownie za kilka sekund.", resetNow: "Przywróć teraz" }, successReset: { delay: 90 * 1000, statusKey: "papermod_comment_success_status" } }; const storage = { getItem: (key) => { try { return localStorage.getItem(key); } catch (e) { console.warn("Failed to read LocalStorage", e); return null; } }, setItem: (key, value) => { try { localStorage.setItem(key, value); } catch (e) { console.warn("Failed to write to LocalStorage", e); } }, removeItem: (key) => { try { localStorage.removeItem(key); } catch (e) { console.warn("Failed to delete from LocalStorage", e); } } }; const STORAGE_KEY = `${CONFIG.storage.prefix}draft_v1`; function getDeviceId() { try { return btoa(encodeURIComponent(navigator.userAgent)).substring(0, 32); } catch (e) { return Math.random().toString(36).substring(2, 10); } } const LIMIT_KEY = `${CONFIG.storage.prefix}rate_${getDeviceId()}_v1`; const SUCCESS_STATUS_KEY = CONFIG.successReset.statusKey; const root = document.getElementById("papermod-comment-root"); const postId = root ? root.dataset.postId : ''; const postTitle = root ? root.dataset.postTitle : ''; const postDescription = document.querySelector('meta[name="description"]')?.content || (root ? root.dataset.postDescription : ''); const wordcountElement = document.getElementById('comment-wordcount-number'); const wordcountContainer = document.getElementById('papermod-comment-wordcount'); const overlimitElement = document.getElementById('papermod-comment-overlimit'); const sensitiveTipElement = document.getElementById('papermod-sensitive-tip'); const submitStatusElement = document.getElementById('papermod-submit-status'); const clearCacheBtn = document.getElementById('papermod-clear-comment-cache'); const form = document.getElementById("papermod-comment-form"); const input = document.getElementById("papermod-comment-input"); const submitBtn = document.getElementById("papermod-comment-submit"); let resetTimer = null; let countdownInterval = null; let draftSaveTimeout = null; if (!form || !input || !submitBtn) { console.warn("Core DOM elements of the comment component are missing"); if (root) { root.innerHTML = `<div style="padding: 2rem; text-align: center; font-family: system-ui; max-width: 720px; margin: 2rem auto; background: #fff; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.08);"> <p style="color: #666; font-size: .9rem;">The comment function is temporarily unavailable. Please try again later.</p> </div>`; } return; } let submitLock = false; function sanitizeInput(text) { if (!text) return ''; let cleanText = text .replace(/\x3Cscript\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '[Filtered malicious content]') .replace(/on\w+\s*=/gi, 'data-on=') .replace(/javascript:/gi, '[Dangerous link]') .replace(/data:/gi, '[Dangerous link]') .replace(/vbscript:/gi, '[Dangerous link]'); cleanText = cleanText .replace(/&/g, '&') .replace(/</g, '<') .replace(/>/g, '>') .replace(/"/g, '"') .replace(/'/g, '''); return cleanText; } function sanitizeHtmlForBot(html) { if (!html) return ''; html = html.replace(/\son\w+\s*=\s*["'][^"']*["']/gi, ''); html = html.replace(/<img[^>]*src="([^"]+)"[^>]*>/gi, (match, src) => { if (/^https?:\/\/.*top10fans\.us/.test(src)) { return `<img src="${src}" alt="Customer Service Image" style="max-width:100%; border-radius:8px;">`; } return `<span>[Invalid Image]</span>`; }); return html.replace(/<(\/?)([a-z0-9]+)([^>]*)>/gi, (match, slash, tagName, attrs) => { tagName = tagName.toLowerCase(); const allowed = ['br', 'img', 'p', 'strong', 'div']; if (allowed.includes(tagName)) { return `<${slash}${tagName}${attrs}>`; } return match.replace(/</g, '<').replace(/>/g, '>'); }); } function detectSensitiveInfo(text) { if (!text) return false; const idCardReg = /\b\d{17}[0-9Xx]\b/i; const bankCardReg = /\b\d{16,19}\b/; const phoneReg = /(^|\D)(1[3-9]\d{9})($|\D)/; return idCardReg.test(text) || bankCardReg.test(text) || phoneReg.test(text); } function updateWordCount() { if (!input || !wordcountElement) return; const currentLength = input.value.length; wordcountElement.textContent = currentLength; const isOverLimit = currentLength > CONFIG.limits.maxCharacters; if (isOverLimit) { wordcountElement.classList.add('comment-wordcount-overlimit'); input.classList.add('overlimit'); submitBtn.disabled = true; overlimitElement.style.display = 'block'; wordcountContainer.style.display = 'none'; sensitiveTipElement.style.display = 'none'; } else { wordcountElement.classList.remove('comment-wordcount-overlimit'); input.classList.remove('overlimit'); submitBtn.disabled = false; overlimitElement.style.display = 'none'; wordcountContainer.style.display = 'block'; const hasSensitive = detectSensitiveInfo(input.value); sensitiveTipElement.style.display = hasSensitive ? 'block' : 'none'; if (currentLength >= 280) { wordcountElement.classList.add('comment-wordcount-overlimit'); } } submitStatusElement.style.display = 'none'; } function loadCommentDraft() { const draft = storage.getItem(STORAGE_KEY); if (draft) { try { const { content, expire } = JSON.parse(draft); if (Date.now() < expire && !detectSensitiveInfo(content)) { input.value = content; updateWordCount(); } else { storage.removeItem(STORAGE_KEY); } } catch (e) { console.error("Failed to load comment draft", e); storage.removeItem(STORAGE_KEY); } } } function saveCommentDraft() { const content = input.value; if (!detectSensitiveInfo(content) && content) { storage.setItem(STORAGE_KEY, JSON.stringify({ content, expire: Date.now() + CONFIG.storage.expireMs })); } else { storage.removeItem(STORAGE_KEY); } } function getSubmitHistory() { const raw = storage.getItem(LIMIT_KEY); if (!raw) return []; try { const arr = JSON.parse(raw); return Array.isArray(arr) ? arr : []; } catch { return []; } } function saveSubmitHistory(arr) { storage.setItem(LIMIT_KEY, JSON.stringify(arr)); } function canSubmitNow() { const now = Date.now(); const history = getSubmitHistory().filter(ts => now - ts <= CONFIG.limits.rateWindowMs); saveSubmitHistory(history); return history.length < CONFIG.limits.rateLimit && !submitLock; } function recordSubmit() { const now = Date.now(); const history = getSubmitHistory().filter(ts => now - ts <= CONFIG.limits.rateWindowMs); history.push(now); saveSubmitHistory(history); } function showSubmitStatus(text, type = "default") { submitStatusElement.textContent = text; submitStatusElement.style.display = 'block'; switch(type) { case "success": submitStatusElement.style.color = CONFIG.colors.success; break; case "error": submitStatusElement.style.color = CONFIG.colors.danger; break; case "warning": submitStatusElement.style.color = CONFIG.colors.warning; break; default: submitStatusElement.style.color = CONFIG.colors.gray; } } function clearCommentDraft() { input.value = ''; storage.removeItem(STORAGE_KEY); updateWordCount(); showSubmitStatus(CONFIG.texts.clearSuccess, "success"); } function renderSuccessMessage(msgHtml, remainingTime = CONFIG.successReset.delay) { if (document.getElementById("papermod-comment-success")) { return; } input.disabled = true; submitBtn.disabled = true; const safeMsgHtml = sanitizeHtmlForBot(msgHtml); const successContainer = document.createElement("div"); successContainer.id = "papermod-comment-success"; successContainer.style = ` width:100%; min-height: 100px; border-radius:8px; border:1px solid ${CONFIG.colors.success}; padding:.7rem .8rem; font-size:.9rem; box-sizing:border-box; margin-bottom:.5rem; background: #f0fff4; color: #007e33; line-height: 1.5; `; const contentWrapper = document.createElement("div"); contentWrapper.innerHTML = safeMsgHtml; const countdownWrapper = document.createElement("div"); countdownWrapper.style = ` margin-top: 1rem; display: flex; align-items: center; justify-content: space-between; font-size: .8rem; color: #006628; `; const remainingSeconds = Math.ceil(remainingTime / 1000); const countdownText = document.createElement("span"); countdownText.id = "papermod-countdown-text"; countdownText.textContent = `${remainingSeconds} ${CONFIG.texts.successCountdown}`; const resetBtn = document.createElement("button"); resetBtn.innerText = CONFIG.texts.resetNow; resetBtn.style = ` padding: .4rem 1rem; border: none; border-radius: 4px; background: ${CONFIG.colors.primary}; color: #fff; cursor: pointer; font-size: .8rem; transition: background 0.2s ease; `; resetBtn.onmouseover = () => { resetBtn.style.background = CONFIG.colors.primaryHover; }; resetBtn.onmouseout = () => { resetBtn.style.background = CONFIG.colors.primary; }; resetBtn.onclick = resetCommentForm; countdownWrapper.appendChild(countdownText); countdownWrapper.appendChild(resetBtn); successContainer.appendChild(contentWrapper); successContainer.appendChild(countdownWrapper); input.style.display = "none"; input.parentNode.insertBefore(successContainer, input); const statusData = { showSuccess: true, msgHtml: safeMsgHtml, startTime: Date.now(), expireTime: Date.now() + remainingTime }; storage.setItem(SUCCESS_STATUS_KEY, JSON.stringify(statusData)); startCountdown(remainingSeconds); if (resetTimer) clearTimeout(resetTimer); resetTimer = setTimeout(() => { resetCommentForm(); }, remainingTime); } function startCountdown(remainingSeconds) { if (countdownInterval) { clearInterval(countdownInterval); } const updateCountdown = () => { remainingSeconds--; const countdownText = document.getElementById('papermod-countdown-text'); if (countdownText) { countdownText.textContent = `${remainingSeconds} ${CONFIG.texts.successCountdown}`; } const statusStr = storage.getItem(SUCCESS_STATUS_KEY); if (statusStr && remainingSeconds > 0) { try { const status = JSON.parse(statusStr); status.expireTime = Date.now() + (remainingSeconds * 1000); storage.setItem(SUCCESS_STATUS_KEY, JSON.stringify(status)); } catch (e) { console.warn("Failed to update countdown cache", e); } } if (remainingSeconds <= 0) { clearInterval(countdownInterval); countdownInterval = null; } }; countdownInterval = setInterval(updateCountdown, 1000); } function resetCommentForm() { if (resetTimer) { clearTimeout(resetTimer); resetTimer = null; } if (countdownInterval) { clearInterval(countdownInterval); countdownInterval = null; } const successContainer = document.getElementById("papermod-comment-success"); if (successContainer) { successContainer.remove(); } input.style.display = "block"; input.disabled = false; input.value = ""; submitBtn.disabled = false; storage.removeItem(SUCCESS_STATUS_KEY); updateWordCount(); showSubmitStatus(""); } function restoreSuccessState() { const statusStr = storage.getItem(SUCCESS_STATUS_KEY); if (!statusStr) return; try { const status = JSON.parse(statusStr); if (status.showSuccess && status.msgHtml && status.expireTime) { const now = Date.now(); const remaining = status.expireTime - now; if (remaining > 0) { renderSuccessMessage(status.msgHtml, remaining); } else { resetCommentForm(); } } } catch (e) { console.error("Failed to restore comment status", e); storage.removeItem(SUCCESS_STATUS_KEY); } } try { loadCommentDraft(); restoreSuccessState(); updateWordCount(); window.addEventListener('beforeunload', saveCommentDraft); clearCacheBtn.addEventListener('click', () => { if (confirm(CONFIG.texts.clearConfirm)) { clearCommentDraft(); } }); input.addEventListener("input", () => { updateWordCount(); clearTimeout(draftSaveTimeout); draftSaveTimeout = setTimeout(saveCommentDraft, 500); }); input.addEventListener("keydown", (e) => { const allowedKeys = ['Backspace', 'Delete', 'ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Tab', 'Meta', 'Control', 'Shift']; if (input.value.length >= CONFIG.limits.maxCharacters && !allowedKeys.includes(e.key) && !e.ctrlKey && !e.metaKey) { e.preventDefault(); } if ((e.ctrlKey || e.metaKey) && e.key === "Enter") { e.preventDefault(); submitBtn.click(); } }); input.addEventListener("paste", (e) => { e.preventDefault(); const clipboardData = e.clipboardData || window.clipboardData; let pastedText = clipboardData.getData('text') || ''; if (detectSensitiveInfo(pastedText)) { sensitiveTipElement.style.display = 'block'; showSubmitStatus(CONFIG.texts.sensitiveReject, "warning"); return; } const maxAdd = CONFIG.limits.maxCharacters - input.value.length; if (maxAdd <= 0) return; input.value += pastedText.substring(0, maxAdd); updateWordCount(); saveCommentDraft(); }); form.addEventListener("submit", async function (e) { e.preventDefault(); let text = (input.value || "").trim(); text = sanitizeInput(text); if (!text) { showSubmitStatus(CONFIG.texts.emptyComment, "warning"); input.focus(); return; } if (text.length > CONFIG.limits.maxCharacters) { showSubmitStatus(CONFIG.texts.overLimit.replace('{max}', CONFIG.limits.maxCharacters), "error"); updateWordCount(); return; } if (detectSensitiveInfo(text)) { showSubmitStatus(CONFIG.texts.sensitiveReject, "warning"); return; } if (!canSubmitNow()) { showSubmitStatus(submitLock ? CONFIG.texts.repeatSubmit : CONFIG.texts.rateLimitTip, "error"); return; } submitLock = true; submitBtn.disabled = true; submitBtn.innerHTML = '<svg width="20" height="20" viewBox="0 0 24 24" aria-hidden="true" style="display: block; height: 60%; width: auto; margin-right: 0.5rem;"><path d="M12 2C6.477 2 2 6.477 2 12s4.477 10 10 10 10-4.477 10-10S17.523 2 12 2zm0 18c-4.411 0-8-3.589-8-8s3.589-8 8-8 8 3.589 8 8-3.589 8-8 8z"/><path d="M13 7h-2v6h6v-2h-4z" fill="currentColor"/></svg><span>Submitting...</span>'; try { const commentData = { msg_type: "comment", message: text, userAgent: navigator.userAgent, ts: new Date().toISOString(), page: window.location.href, lang: (document.documentElement.lang || "en").toString().toLowerCase(), ext: { postId: postId, postTitle: postTitle, postDescription: postDescription } }; const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), CONFIG.api.timeout); const res = await fetch(CONFIG.api.endpoint, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(commentData), signal: controller.signal }); clearTimeout(timeoutId); if (res.status === 429) { const data = await res.json(); showSubmitStatus(data.msg || "Submission is too frequent, please try again later", "error"); submitLock = false; submitBtn.disabled = false; submitBtn.innerHTML = '<svg width="20" height="20" viewBox="0 0 24 24" aria-hidden="true" style="display: block; height: 60%; width: auto; margin-right: 0.5rem;"><path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z" fill="currentColor"/></svg><span>Post</span>'; return; } if (!res.ok) throw new Error("HTTP " + res.status); const data = await res.json(); recordSubmit(); if (data.code === 200 && data.msg) { renderSuccessMessage(data.msg); } else { showSubmitStatus(CONFIG.texts.sendSuccess, "success"); input.value = ''; storage.removeItem(STORAGE_KEY); updateWordCount(); } } catch (err) { console.error("Failed to submit comment:", err); const msg = (err && typeof err.message === "string") ? err.message : ""; if (err && err.name === "AbortError") { showSubmitStatus("Request timed out, please check your network and try again", "error"); } else if (msg.includes("HTTP")) { showSubmitStatus(`Server error(${msg}), please try again later`, "error"); } else { showSubmitStatus(CONFIG.texts.sendFailed, "error"); } } finally { setTimeout(() => { submitLock = false; if (!document.getElementById("papermod-comment-success")) { submitBtn.disabled = false; } submitBtn.innerHTML = '<svg width="20" height="20" viewBox="0 0 24 24" aria-hidden="true" style="display: block; height: 60%; width: auto; margin-right: 0.5rem;"><path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z" fill="currentColor"/></svg><span>Post</span>'; }, 1500); } }); } catch (err) { console.error("Failed to initialize comment component:", err); if (root) { root.innerHTML = `<div style="padding: 2rem; text-align: center; font-family: system-ui; max-width: 720px; margin: 2rem auto; background: #fff; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.08);"> <p style="color: #666; font-size: .9rem;">The comment function is temporarily unavailable. Please try again later.</p> </div>`; } } })(); </script> <div id="ai-comments" class="ai-comments" data-slug="onlyfans-logo-svg-1818"> <div class="ai-comments__inner"> <h2 class="ai-comments__title"> 💬 Wybrane komentarze <a href="/comments/onlyfans-logo-svg-1818/" style=" font-style: italic; text-decoration: none; margin-left: 8px; vertical-align: middle; font-size: .85rem; color: #667085; font-weight: normal; font-family: inherit; line-height: inherit; "> [ Zobacz wszystkie komentarze ] </a> </h2> <p class="ai-comments__subtitle"> Poniższe komentarze zostały edytowane i dopracowane przez AI wyłącznie w celach referencyjnych i do dyskusji. </p> <div class="ai-comments__list" id="ai-comments-list"> <div class="ai-comments__loading">Ładowanie komentarzy...</div> </div> </div> </div> <style> .ai-comments { margin-top: 3rem; margin-bottom: 1.5rem; } .ai-comments__inner { max-width: var(--content-width, 750px); margin: 0 auto; padding: 1.25rem 0.75rem 1.5rem; border-radius: 12px; border: 1px solid #e0e7ff; background: #f8faff; } .ai-comments__title { margin: 0 0 .25rem; font-size: 1.1rem; font-weight: 600; color: #ff4081; display: flex; align-items: center; gap: .35rem; } .ai-comments__subtitle { margin: 0 0 .9rem; font-size: .85rem; color: #667085; } .ai-comments__list { display: flex; flex-direction: column; gap: .85rem; } .ai-comments__loading, .ai-comments__empty, .ai-comments__error { font-size: .9rem; color: #667085; } .ai-comments__error { color: #d92d20; } .ai-comment-thread { padding: .75rem .85rem; border-radius: 10px; background: #ffffff; border: 1px solid #e4e7ec; } .ai-comment-thread + .ai-comment-thread { } .ai-comment-card { display: block; padding: .45rem 0; border-bottom: 1px solid #eef0f4; } .ai-comment-card:last-child { border-bottom: none; } .ai-comment-card--author { } .ai-comment-card--child { } .ai-comment-header { display: flex; align-items: flex-start; gap: .55rem; margin-bottom: .15rem; } .ai-comment-avatar { flex-shrink: 0; width: 35px; height: 35px; border-radius: 999px; overflow: hidden; display: flex; align-items: center; justify-content: center; background: #ff4081; color: #fff; font-weight: 700; font-size: .95rem; text-transform: uppercase; } .ai-comment-avatar img { width: 100%; height: 100%; object-fit: cover; display: block; } .ai-comment-body { flex: 1; min-width: 0; } .ai-comment-meta { display: flex; flex-direction: column; gap: .15rem; } .ai-comment-meta-top { display: flex; flex-wrap: wrap; align-items: center; gap: .35rem; } .ai-comment-name { font-size: .9rem; font-weight: 600; color: #111827; } .ai-comment-name--author { color: #1d4ed8; } .ai-comment-badge { font-size: .65rem; padding: .05rem .35rem; border-radius: 999px; background: rgba(66, 133, 244, .08); color: #ff4081; border: 1px solid rgba(66, 133, 244, .25); } .ai-comment-date { font-size: .75rem; color: #9ca3af; } .ai-comment-text { margin-top: .2rem; font-size: .9rem; line-height: 1.6; color: #374151; white-space: pre-wrap; word-wrap: break-word; } .ai-comment-actions { margin-top: .15rem; display: flex; flex-wrap: wrap; gap: .25rem; } .ai-comment-replies { margin-top: .3rem; display: flex; flex-direction: column; } .ai-comment-toggle { padding: 0.12rem 0.55rem; font-size: 0.78rem; border-radius: 999px; border: 1px solid #e5e7eb; background: #f9fafb; color: #4b5563; cursor: pointer; } .ai-comment-toggle:hover { background: #f3f4f6; } @media (max-width: 600px) { .ai-comments__inner { padding-inline: .75rem; } } </style> <script> (function() { const container = document.getElementById('ai-comments'); if (!container) return; const listEl = document.getElementById('ai-comments-list'); const slug = container.dataset.slug; if (!slug) { listEl.innerHTML = '<div class="ai-comments__empty">No comments yet.</div>'; return; } const COMMENTS_PATH = '/data/comments/' + slug + '.json'; function formatDate(raw) { if (!raw) return ''; const s = String(raw).trim(); const m = s.match(/^(\d{4}-\d{2}-\d{2})[-\s](\d{2}:\d{2})$/); if (m) return m[1] + ' ' + m[2]; return s; } function createAvatar(name, avatar) { const wrapper = document.createElement('div'); wrapper.className = 'ai-comment-avatar'; const FALLBACK_URL = '/images/og.webp'; if (avatar && typeof avatar === 'string') { const trimmed = avatar.trim(); if (!/^<\w+[\s>]/.test(trimmed)) { const img = document.createElement('img'); img.src = trimmed; img.alt = name || 'avatar'; img.loading = 'lazy'; img.onerror = function () { img.onerror = null; img.src = FALLBACK_URL; }; wrapper.appendChild(img); return wrapper; } } const img = document.createElement('img'); img.src = FALLBACK_URL; img.alt = name || 'avatar'; img.loading = 'lazy'; wrapper.appendChild(img); return wrapper; } function pickName(c) { if (!c) return 'Reader'; return c['user-name'] || c['author-name'] || c.name || 'Reader'; } function pickAvatar(c) { if (!c) return null; return ( c['user-avatar'] || c['author-avatar'] || c.avatar || (c.authorType === 'author' ? '/matitie.webp' : null) ); } function pickText(c) { if (!c) return ''; return c.comment || c.answer || c.text || ''; } function pickDateRaw(c) { if (!c) return ''; return c['user-date'] || c['author-date'] || c.date || ''; } function safeParseTime(raw) { if (!raw) return 0; const s = String(raw).trim(); const m = s.match(/^(\d{4}-\d{2}-\d{2})[-\s](\d{2}:\d{2})$/); if (m) { const iso = m[1] + 'T' + m[2] + ':00'; const t = Date.parse(iso); if (!isNaN(t)) return t; } const t2 = Date.parse(s); return isNaN(t2) ? 0 : t2; } function normalizeOldFormat(data) { const arr = Array.isArray(data) ? data : [data]; const threads = []; arr.forEach(function(threadData) { if (!threadData || !Array.isArray(threadData.comments)) return; const commentsArr = threadData.comments; const root = commentsArr[0] || null; const author = commentsArr[1] || null; if (root || author) { threads.push({ root: root, author: author, childrenMap: new Map() }); } }); return { threads: threads, idMap: new Map() }; } function normalizeNewFormat(obj) { const comments = Array.isArray(obj.comments) ? obj.comments : []; const idMap = new Map(); comments.forEach(function(c) { if (c && c.id) { idMap.set(c.id, c); } }); const groups = new Map(); comments.forEach(function(c) { if (!c) return; const rootId = c.rootId || c.id; if (!groups.has(rootId)) groups.set(rootId, []); groups.get(rootId).push(c); }); const threads = []; groups.forEach(function(group) { if (!group.length) return; var root = group.find(function(x) { return (x.replyToId == null || x.replyToId === x.id) && x.authorType !== 'author'; }) || group[0]; var author = group.find(function(x) { return ( x.authorType === 'author' && x.replyToId === root.id ); }) || null; const childrenMap = new Map(); group.forEach(function(c) { if (!c) return; if (c === root) return; const parentId = (c.replyToId != null && c.replyToId !== c.id) ? c.replyToId : null; if (!parentId) return; if (!childrenMap.has(parentId)) { childrenMap.set(parentId, []); } childrenMap.get(parentId).push(c); }); if (author && childrenMap.has(root.id)) { const arr = childrenMap.get(root.id).filter(function(x) { return x.id !== author.id; }); if (arr.length) { childrenMap.set(root.id, arr); } else { childrenMap.delete(root.id); } } childrenMap.forEach(function(list) { list.sort(function(a, b) { const ta = a.dateTimestamp || safeParseTime(pickDateRaw(a)) || 0; const tb = b.dateTimestamp || safeParseTime(pickDateRaw(b)) || 0; return ta - tb; }); }); threads.push({ root: root, author: author, childrenMap: childrenMap }); }); return { threads: threads, idMap: idMap }; } function getThreadTime(thread) { const parts = []; if (thread.root) parts.push(thread.root); if (thread.author) parts.push(thread.author); const childrenMap = thread.childrenMap; if (childrenMap && typeof childrenMap.forEach === 'function') { childrenMap.forEach(function(list) { list.forEach(function(c) { parts.push(c); }); }); } let max = 0; parts.forEach(function(c) { if (!c) return; if (c.dateTimestamp) { if (c.dateTimestamp > max) max = c.dateTimestamp; return; } const raw = pickDateRaw(c); if (!raw) return; const t = safeParseTime(raw); if (t && t > max) max = t; }); return max; } function createChildrenBlock(parentId, childrenMap, idMap, actionsEl, threadEl) { if (!childrenMap || !childrenMap.has(parentId)) return null; const list = childrenMap.get(parentId) || []; if (!list.length) return null; const repliesWrap = document.createElement('div'); repliesWrap.className = 'ai-comment-replies'; repliesWrap.style.display = 'none'; list.forEach(function(reply) { const fromName = pickName(reply); const avatar = pickAvatar(reply); const dateRaw = pickDateRaw(reply); const text = pickText(reply); const safeText = text && String(text).trim(); if (!safeText) { return; } let toName = ''; if (reply.replyToId != null && idMap && idMap.has(reply.replyToId)) { const target = idMap.get(reply.replyToId); toName = pickName(target); } const card = document.createElement('div'); card.className = 'ai-comment-card ai-comment-card--child'; const header = document.createElement('div'); header.className = 'ai-comment-header'; const avatarEl = createAvatar(fromName, avatar); const body = document.createElement('div'); body.className = 'ai-comment-body'; const meta = document.createElement('div'); meta.className = 'ai-comment-meta'; const metaTop = document.createElement('div'); metaTop.className = 'ai-comment-meta-top'; const nameEl = document.createElement('div'); nameEl.className = 'ai-comment-name'; nameEl.textContent = toName ? fromName + ' replied to ' + toName : fromName; metaTop.appendChild(nameEl); meta.appendChild(metaTop); const dateEl = document.createElement('div'); dateEl.className = 'ai-comment-date'; dateEl.textContent = formatDate(dateRaw); if (dateRaw) meta.appendChild(dateEl); const textEl = document.createElement('div'); textEl.className = 'ai-comment-text'; textEl.textContent = safeText; body.appendChild(meta); body.appendChild(textEl); header.appendChild(avatarEl); header.appendChild(body); card.appendChild(header); repliesWrap.appendChild(card); }); if (!repliesWrap.childNodes.length) { return null; } const toggleBtn = document.createElement('button'); toggleBtn.type = 'button'; toggleBtn.className = 'ai-comment-toggle'; toggleBtn.textContent = 'more(' + list.length + ')'; let expanded = false; toggleBtn.addEventListener('click', function () { expanded = !expanded; if (expanded) { repliesWrap.style.display = 'flex'; toggleBtn.textContent = 'Collapse'; } else { repliesWrap.style.display = 'none'; toggleBtn.textContent = 'more(' + list.length + ')'; } }); if (actionsEl) { actionsEl.appendChild(toggleBtn); } if (threadEl) { threadEl.appendChild(repliesWrap); } return { toggleBtn: toggleBtn, repliesWrap: repliesWrap }; } function renderThread(thread, idMap) { const root = thread.root; const author = thread.author; const childrenMap = thread.childrenMap || new Map(); if (!root && !author) return null; const threadEl = document.createElement('div'); threadEl.className = 'ai-comment-thread'; if (root) { const isAuthorRoot = root.authorType === 'author'; const card = document.createElement('div'); card.className = 'ai-comment-card' + (isAuthorRoot ? ' ai-comment-card--author' : ''); const header = document.createElement('div'); header.className = 'ai-comment-header'; const name = pickName(root); const avatar = pickAvatar(root); const dateRaw = pickDateRaw(root); const text = pickText(root); const safeText = text && String(text).trim(); const avatarEl = createAvatar(name, avatar); const body = document.createElement('div'); body.className = 'ai-comment-body'; const meta = document.createElement('div'); meta.className = 'ai-comment-meta'; const metaTop = document.createElement('div'); metaTop.className = 'ai-comment-meta-top'; const nameEl = document.createElement('div'); nameEl.className = 'ai-comment-name' + (isAuthorRoot ? ' ai-comment-name--author' : ''); nameEl.textContent = name; metaTop.appendChild(nameEl); meta.appendChild(metaTop); const dateEl = document.createElement('div'); dateEl.className = 'ai-comment-date'; dateEl.textContent = formatDate(dateRaw); if (dateRaw) meta.appendChild(dateEl); const textEl = document.createElement('div'); textEl.className = 'ai-comment-text'; textEl.textContent = safeText || '(This comment is empty)'; const actionsEl = document.createElement('div'); actionsEl.className = 'ai-comment-actions'; body.appendChild(meta); body.appendChild(textEl); body.appendChild(actionsEl); header.appendChild(avatarEl); header.appendChild(body); card.appendChild(header); threadEl.appendChild(card); createChildrenBlock(root.id, childrenMap, idMap, actionsEl, threadEl); } if (author) { const name = pickName(author) || 'MaTitie'; const avatar = pickAvatar(author) || '/matitie.webp'; const dateRaw = pickDateRaw(author); const text = pickText(author); const safeText = text && String(text).trim(); const card = document.createElement('div'); card.className = 'ai-comment-card ai-comment-card--author'; const header = document.createElement('div'); header.className = 'ai-comment-header'; const avatarEl = createAvatar(name, avatar); const body = document.createElement('div'); body.className = 'ai-comment-body'; const meta = document.createElement('div'); meta.className = 'ai-comment-meta'; const metaTop = document.createElement('div'); metaTop.className = 'ai-comment-meta-top'; const nameEl = document.createElement('div'); nameEl.className = 'ai-comment-name ai-comment-name--author'; nameEl.textContent = name; const badge = document.createElement('div'); badge.className = 'ai-comment-badge'; const rootName = root ? pickName(root) : 'Reader'; badge.textContent = 'Author Reply ' + rootName; metaTop.appendChild(nameEl); metaTop.appendChild(badge); meta.appendChild(metaTop); const dateEl = document.createElement('div'); dateEl.className = 'ai-comment-date'; dateEl.textContent = formatDate(dateRaw); if (dateRaw) meta.appendChild(dateEl); const textEl = document.createElement('div'); textEl.className = 'ai-comment-text'; textEl.textContent = safeText || '(This comment is empty)'; const actionsEl = document.createElement('div'); actionsEl.className = 'ai-comment-actions'; body.appendChild(meta); body.appendChild(textEl); body.appendChild(actionsEl); header.appendChild(avatarEl); header.appendChild(body); card.appendChild(header); threadEl.appendChild(card); createChildrenBlock(author.id, childrenMap, idMap, actionsEl, threadEl); } return threadEl; } function loadComments() { listEl.innerHTML = '<div class="ai-comments__loading">Loading comments...</div>'; fetch(COMMENTS_PATH) .then(function(resp) { if (resp.status === 404) { listEl.innerHTML = '<div class="ai-comments__empty">No comments yet.</div>'; return null; } if (!resp.ok) { throw new Error('HTTP ' + resp.status); } return resp.json(); }) .then(function(data) { if (!data) return; let threads = []; let idMap = new Map(); if (Array.isArray(data) && data.length > 0) { const newObjs = data.filter(function (o) { return ( o && Array.isArray(o.comments) && o.comments.length > 0 && Object.prototype.hasOwnProperty.call(o.comments[0], 'rootId') ); }); const oldObjs = data.filter(function (o) { return ( o && Array.isArray(o.comments) && o.comments.length > 0 && !Object.prototype.hasOwnProperty.call(o.comments[0], 'rootId') ); }); let newThreadsInfo = null; let oldThreadsInfo = null; if (newObjs.length > 0) { const mergedNew = { comments: [] }; const seenIds = new Set(); newObjs.forEach(function (o) { (o.comments || []).forEach(function (c) { if (!c || !c.id) return; if (seenIds.has(c.id)) return; seenIds.add(c.id); mergedNew.comments.push(c); }); }); newThreadsInfo = normalizeNewFormat(mergedNew); } if (oldObjs.length > 0) { oldThreadsInfo = normalizeOldFormat(oldObjs); } const newThreads = newThreadsInfo ? (newThreadsInfo.threads || []) : []; const oldThreads = oldThreadsInfo ? (oldThreadsInfo.threads || []) : []; newThreads.sort(function (a, b) { return getThreadTime(b) - getThreadTime(a); }); oldThreads.sort(function (a, b) { return getThreadTime(b) - getThreadTime(a); }); threads = newThreads.concat(oldThreads); if (newThreadsInfo && newThreadsInfo.idMap) { idMap = newThreadsInfo.idMap; } else if (oldThreadsInfo && oldThreadsInfo.idMap) { idMap = oldThreadsInfo.idMap; } } else { let threadsInfo; let isNewFormat = false; let mergedNew = null; const obj = data; if ( obj && Array.isArray(obj.comments) && obj.comments.length > 0 && Object.prototype.hasOwnProperty.call(obj.comments[0], 'rootId') ) { isNewFormat = true; mergedNew = obj; } if (isNewFormat && mergedNew) { threadsInfo = normalizeNewFormat(mergedNew); } else { threadsInfo = normalizeOldFormat(data); } threads = threadsInfo.threads || []; idMap = threadsInfo.idMap || new Map(); } if (!threads.length) { listEl.innerHTML = '<div class="ai-comments__empty">No comments yet.</div>'; return; } listEl.innerHTML = ''; threads.forEach(function (thread) { const el = renderThread(thread, idMap); if (el) listEl.appendChild(el); }); }) .catch(function(err) { console.error('AI comments load error:', err); listEl.innerHTML = '<div class="ai-comments__error">Comments cannot be loaded temporarily. Please try again later.' + ' <button type="button" id="ai-comments-retry" style="margin-left:.5rem;font-size:.78rem;border:1px solid #e5e7eb;border-radius:999px;padding:0.05rem 0.5rem;background:#f9fafb;cursor:pointer;">Retry</button>' + '</div>'; var retryBtn = document.getElementById('ai-comments-retry'); if (retryBtn) { retryBtn.addEventListener('click', function () { loadComments(); }); } }); } loadComments(); })(); </script> <div class="related-posts"> <h2>Related Posts</h2> <ul> <li style="margin-bottom:1rem;"> <a href="/posts/onlyfans-logo-download-8091/"><strong>OnlyFans logo: jak pobrać, legalnie i szybko?</strong></a><br> <small>2025-08-28</small><br> <p>"Praktyczny przewodnik: gdzie i jak pobrać logo OnlyFans, jakie są ograniczenia prawne oraz najlepsze metody jakościowe."</p> </li> <li style="margin-bottom:1rem;"> <a href="/posts/onlyfans-logo-marketing-0268/"><strong>Marketerzy: gdzie użyć logo OnlyFans (a gdzie nie)</strong></a><br> <small>2025-08-12</small><br> <p>Jak bezpiecznie i skutecznie używać logo OnlyFans w 2025 r.: zasady, ryzyka, case study ze sportu, trendy popytu i praktyczne wskazówki dla twórców i marek w Polsce.</p> </li> <li style="margin-bottom:1rem;"> <a href="/posts/onlyfans-logo-generator-pl-7145/"><strong>Polscy Twórcy Na OnlyFans: Logo Generator, Który Odmieni Twój Profil!</strong></a><br> <small>2025-06-23</small><br> <p>Jak polscy twórcy OnlyFans wykorzystują logo generatory, by wyróżnić się i zdobyć subskrybentów. Przegląd trendów, narzędzi i opinii.</p> </li> <li style="margin-bottom:1rem;"> <a href="/posts/onlyfans-font-polscy-tworcy-8765/"><strong>Polscy Twórcy na OnlyFans: Dlaczego Czcionka To Twój Tajny Oręż (I Jak Może Zmienić Wszystko)?</strong></a><br> <small>2025-06-21</small><br> <p>Czcionka na OnlyFans – jak wybrać font, który przyciąga subskrybentów i wyróżnia profil wśród polskich twórców.</p> </li> </ul> </div> <footer class="post-footer"> </footer> </article> </main> <footer class="footer" style="text-align:center;padding:2rem 0;color:#666;"> <div class="footer-links" style="margin-bottom:.5rem;line-height:1.6;"> <a href="/about/">O nas</a> | <a href="/privacy-policy/">Polityka prywatności</a> | <a href="/terms-of-use/">Regulamin</a> | <a href="/contact-us/">Kontakt</a> </div> <small> © 2026 Top10Fans  ·  CC BY 4.0 </small> </footer> <div id="lvga-chat-root"> <button id="lvga-chat-toggle" aria-label="Open support chat" tabindex="0" style=" position: fixed; right: 1.5rem; bottom: 1.5rem; width: 52px; height: 52px; border-radius: 50%; border: none; cursor: pointer; box-shadow: 0 4px 12px rgba(0,0,0,0.18); display: flex; align-items: center; justify-content: center; z-index: 9999; background: #ff4081; /* 主色:粉色 */ color: #fff; transition: all 0.2s ease; "> <span id="lvga-chat-icon-chat" style="line-height:1; display:flex; align-items:center; justify-content:center;"> <span class="lvga-dot lvga-dot-1"></span> <span class="lvga-dot lvga-dot-2"></span> <span class="lvga-dot lvga-dot-3"></span> </span> <span id="lvga-chat-icon-close" style="font-size: 22px; line-height: 1; display:none;" tabindex="0">✕</span> </button> <div id="lvga-chat-panel" style=" position: fixed; right: 1.5rem; bottom: 5.5rem; width: min(360px, 92vw); height: auto; max-height: 80vh; background: #fff; border-radius: 12px; box-shadow: 0 10px 30px rgba(0,0,0,0.18); display: none; flex-direction: column; overflow: hidden; z-index: 9998; font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; "> <div style=" padding: .85rem 1rem; background: #ff4081; color: #fff; display:flex; align-items:center; justify-content:space-between; "> <div style="font-size:.95rem; font-weight:600;">Support</div> <button id="lvga-clear-history" style=" background: transparent; border: none; color: #fff; cursor: pointer; font-size: .75rem; padding: .2rem .4rem; border-radius: 4px; transition: background 0.2s ease; "> Clear History </button> </div> <div id="lvga-chat-messages" style=" flex: 1; padding: .75rem .75rem 0; overflow-y: auto; background: #f7f7f7; font-size: .85rem; min-height: 120px; max-height: calc(80vh - 120px); "> <div style="margin-bottom:.75rem; display:flex;"> <div style=" max-width:80%; padding:.5rem .7rem; border-radius:12px 12px 12px 0; background:#fff; box-shadow:0 1px 3px rgba(0,0,0,0.06); "> 👋 Cześć, jestem MaTitie. Zwróć uwagę, że mogę nie być dostępny w czasie rzeczywistym. </div> </div> <div id="lvga-privacy-tip" style=" margin-bottom:.75rem; display: none; justify-content:center; "> <div style=" max-width:90%; padding:.4rem .6rem; border-radius:8px; background:#ffe9f3; /* 淡粉背景 */ color:#ff4081; /* 品牌粉文字 */ font-size:.75rem; text-align:center; "> Szyfrowany czat. Pamięć lokalna jest automatycznie czyszczona po 24 godzinach. </div> </div> </div> <form id="lvga-chat-form" style="border-top:1px solid #eee; padding:.5rem .6rem .6rem; background:#fff;"> <textarea id="lvga-chat-input" rows="2" placeholder="Wpisz tutaj swoje zapytanie" style=" width:100%; resize:none; border-radius:8px; border:1px solid #ddd; padding:.45rem .55rem; font-size:.85rem; box-sizing:border-box; margin-bottom:.35rem; transition: border-color 0.2s ease; "></textarea> <div id="lvga-chat-wordcount" style=" text-align: right; font-size: .75rem; color: #666; margin-top: -0.2rem; margin-bottom: .5rem; height: 1rem; "> <span id="wordcount-number">0</span>/300 </div> <div id="lvga-chat-overlimit" style=" text-align: center; font-size: .75rem; color: #ff4444; margin-top: -0.2rem; margin-bottom: .5rem; display: none; "> Max 300 characters. Shorten and retry. </div> <div id="lvga-sensitive-tip" style=" text-align: center; font-size: .75rem; color: #ff9900; margin-top: -0.2rem; margin-bottom: .5rem; display: none; "> No sensitive info (ID / bank card numbers, etc.). </div> <div style=" display:flex; justify-content:center; margin-top:.55rem; margin-bottom:.65rem; "> <button type="submit" id="lvga-chat-submit" style=" padding: 0.6rem 1.6rem; min-height: 44px; min-width: 120px; border-radius:999px; border:none; font-size: 0.8rem; cursor:pointer; background:#ff4081; color:#fff; display:flex; align-items:center; justify-content:center; box-shadow: 0 2px 4px rgba(0,0,0,0.05); transition: all 0.2s ease; "> <svg width="20" height="20" viewBox="0 0 24 24" aria-hidden="true" style="display: block; height: 60%; width: auto; margin-right: 0.4rem;"> <path d="M3 11.5L20 3L15 21L11.5 13L3 11.5Z" fill="currentColor" /> </svg> <span>Send</span> </button> </div> </form> </div> </div> <style> #lvga-chat-toggle .lvga-dot { display: inline-block; width: 6px; height: 6px; border-radius: 50%; background: #ffffff; margin: 0 1px; animation: lvga-dot-pulse 1s infinite ease-in-out; } #lvga-chat-toggle .lvga-dot-2 { animation-delay: .2s; } #lvga-chat-toggle .lvga-dot-3 { animation-delay: .4s; } @keyframes lvga-dot-pulse { 0%, 100% { background: #ffffff; } 40% { background: #ff80ab; } } #lvga-chat-submit:hover { background: #e73370; transform: scale(1.02); box-shadow: 0 3px 6px rgba(0,0,0,0.08); } #lvga-chat-submit:active { background: #c62862; transform: scale(0.98); } #lvga-chat-submit:disabled { background: #f8a6c2; cursor: not-allowed; transform: none; box-shadow: none; } #lvga-chat-input:focus { border-color: #ff4081; outline: none; box-shadow: 0 0 0 2px rgba(255, 64, 129, 0.15); } #lvga-chat-toggle:hover { background: #e73370; transform: scale(1.05); } #lvga-chat-toggle:active { background: #c62862; transform: scale(0.98); } .wordcount-overlimit { color: #ff4444 !important; font-weight: 500; } #lvga-chat-input.overlimit { border-color: #ff4444 !important; box-shadow: 0 0 0 2px rgba(255, 68, 68, 0.1) !important; } #lvga-clear-history:hover { background: rgba(255,255,255,0.2); } @media (max-height: 500px) { #lvga-chat-panel { max-height: 50vh; } } @media (max-width: 767px) { #lvga-chat-panel { max-height: 65vh !important; } #lvga-chat-messages { max-height: calc(65vh - 120px) !important; } } @media (min-width: 768px) { #lvga-chat-panel { bottom: 2rem; right: 2rem; } #lvga-chat-toggle { bottom: 2rem; right: 2rem; } } </style> <script> (function () { const CONFIG = { colors: { primary: "#ff4081", primaryHover: "#e73370", primaryActive: "#c62862", danger: "#ff4444", warning: "#ff9900", gray: "#666" }, api: { endpoint: "https://chat.top10fans.us/dingtalk-chat", timeout: 10000 }, limits: { maxCharacters: 300, rateLimit: 3, rateWindowMs: 5 * 60 * 1000, countdownMs: 60 * 1000 }, storage: { prefix: "lvga_chat_", expireMs: 24 * 60 * 60 * 1000 }, texts: { privacyTip: "This chat is transmitted securely, and your chat history will be automatically cleared after 24 hours.", overLimit: "You can enter up to {max} characters. Please shorten your message and try again.", sensitiveTip: "Do not send sensitive personal information such as ID numbers or bank card details.", repeatSubmit: "Please do not submit repeatedly. Try again in a moment.", rateLimitTip: "Live support may not be online 😂\nYou may email us directly at support@top10fans.us", sendFailed: "Message failed to send. Please try again later or email support@top10fans.us", clearConfirm: "Are you sure you want to clear all chat history?", clearSuccess: "Chat history has been cleared.", sensitiveReject: "Do not send sensitive information such as ID numbers, bank card numbers, or phone numbers." } }; const STORAGE_KEY = `${CONFIG.storage.prefix}history_v1`; const inputKey = `${CONFIG.storage.prefix}input_v1`; const COUNTDOWN_TS_KEY = `${CONFIG.storage.prefix}countdown_ts_v1`; const OPEN_KEY = `${CONFIG.storage.prefix}open_v1`; const PRIVACY_SHOWN_KEY = `${CONFIG.storage.prefix}privacy_shown_v1`; const STORAGE_EXPIRE_KEY = `${CONFIG.storage.prefix}expire_v1`; function getDeviceFingerprint() { const fingerprint = [ navigator.userAgent, screen.width + "x" + screen.height, navigator.language, screen.colorDepth ].join("|"); return btoa(fingerprint).substring(0, 32); } const LIMIT_KEY = `${CONFIG.storage.prefix}rate_${getDeviceFingerprint()}_v1`; const wordcountElement = document.getElementById('wordcount-number'); const wordcountContainer = document.getElementById('lvga-chat-wordcount'); const overlimitElement = document.getElementById('lvga-chat-overlimit'); const sensitiveTipElement = document.getElementById('lvga-sensitive-tip'); const privacyTipElement = document.getElementById('lvga-privacy-tip'); const clearHistoryBtn = document.getElementById('lvga-clear-history'); const btn = document.getElementById("lvga-chat-toggle"); const panel = document.getElementById("lvga-chat-panel"); const iconChat = document.getElementById("lvga-chat-icon-chat"); const iconClose = document.getElementById("lvga-chat-icon-close"); const form = document.getElementById("lvga-chat-form"); const input = document.getElementById("lvga-chat-input"); const messages = document.getElementById("lvga-chat-messages"); const submitBtn = document.getElementById("lvga-chat-submit"); if (!btn || !panel || !form || !input) { console.warn("Chat core DOM element missing"); return; } const htmlLang = (document.documentElement.lang || "en").toString().toLowerCase(); const pageLang = htmlLang.split("-")[0] || "en"; let countdownTimer = null; let countdownBubble = null; let submitLock = false; function scrollToBottom() { messages.scrollTop = messages.scrollHeight; } function sanitizeInput(text) { if (!text) return ''; let cleanText = text .replace(/\x3Cscript\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '[Filtered malicious content]') .replace(/on\w+\s*=/gi, 'data-on=') .replace(/javascript:/gi, '[Dangerous link]') .replace(/data:/gi, '[Dangerous link]') .replace(/vbscript:/gi, '[Dangerous link]'); cleanText = cleanText .replace(/&/g, '&') .replace(/</g, '<') .replace(/>/g, '>') .replace(/"/g, '"') .replace(/'/g, '''); return cleanText; } function sanitizeHtmlForBot(html) { if (!html) return ''; html = html.replace(/\son\w+\s*=\s*["'][^"']*["']/gi, ''); html = html.replace(/<img[^>]*src="([^"]+)"[^>]*>/gi, (match, src) => { if (/^https?:\/\/.*top10fans\.us/.test(src)) { return `<img src="${src}" alt="Customer Service Image" style="max-width:100%; border-radius:8px;">`; } return `<span>[无效图片]</span>`; }); return html.replace(/<(\/?)([a-z0-9]+)([^>]*)>/gi, (match, slash, tagName, attrs) => { tagName = tagName.toLowerCase(); const allowed = ['br', 'img', 'p', 'strong', 'div']; if (allowed.includes(tagName)) { return `<${slash}${tagName}${attrs}>`; } return match.replace(/</g, '<').replace(/>/g, '>'); }); } function detectSensitiveInfo(text) { if (!text) return false; const idCardReg = /\b\d{17}[\dXx]\b/; const bankCardReg = /\b\d{16,19}\b/; const phoneReg = /\b1[3-9]\d{9}\b/; return idCardReg.test(text) || bankCardReg.test(text) || phoneReg.test(text); } function updateWordCount() { if (!input || !wordcountElement) return; const currentLength = input.value.length; wordcountElement.textContent = currentLength; const isOverLimit = currentLength > CONFIG.limits.maxCharacters; if (isOverLimit) { wordcountElement.classList.add('wordcount-overlimit'); input.classList.add('overlimit'); submitBtn.disabled = true; overlimitElement.style.display = 'block'; wordcountContainer.style.display = 'none'; sensitiveTipElement.style.display = 'none'; } else { wordcountElement.classList.remove('wordcount-overlimit'); input.classList.remove('overlimit'); submitBtn.disabled = false; overlimitElement.style.display = 'none'; wordcountContainer.style.display = 'block'; const hasSensitive = detectSensitiveInfo(input.value); sensitiveTipElement.style.display = hasSensitive ? 'block' : 'none'; if (currentLength >= 280) { wordcountElement.classList.add('wordcount-overlimit'); } } } function loadFromCache() { const expireTs = localStorage.getItem(STORAGE_EXPIRE_KEY); if (expireTs && Date.now() > Number(expireTs)) { localStorage.removeItem(STORAGE_KEY); localStorage.removeItem(STORAGE_EXPIRE_KEY); return; } const cached = localStorage.getItem(STORAGE_KEY); if (cached) { try { const arr = JSON.parse(cached); arr.forEach((msg) => { if (msg.isHtml) { appendHtmlMessage(msg.text, msg.from, false); } else { appendMessage(msg.text, msg.from, false); } }); } catch (e) { console.error("Load cache failed", e); localStorage.removeItem(STORAGE_KEY); } } const cachedInput = localStorage.getItem(inputKey); if (cachedInput) { if (cachedInput.length > CONFIG.limits.maxCharacters) { input.value = cachedInput.substring(0, CONFIG.limits.maxCharacters); } else { if (!detectSensitiveInfo(cachedInput)) { input.value = cachedInput; } else { input.value = ''; } } updateWordCount(); } scrollToBottom(); const ts = localStorage.getItem(COUNTDOWN_TS_KEY); if (ts) { const start = Number(ts); if (!Number.isNaN(start)) { const elapsed = Math.floor((Date.now() - start) / 1000); const remain = 60 - elapsed; if (remain > 0) { appendCountdownMessage(remain); } else { appendCountdownMessage(0); } } } } function saveToCache() { const bubbles = messages.querySelectorAll("div[chat-bubble]"); const arr = []; bubbles.forEach((b) => { if (b.getAttribute("data-countdown") === "1" || b.closest('#lvga-privacy-tip')) return; const from = b.getAttribute("chat-bubble"); const html = b.getAttribute("data-html"); let content = html || b.innerText; if (detectSensitiveInfo(content)) return; if (html) arr.push({ text: content, from, isHtml: true }); else arr.push({ text: content, from, isHtml: false }); }); localStorage.setItem(STORAGE_KEY, JSON.stringify(arr)); localStorage.setItem(STORAGE_EXPIRE_KEY, (Date.now() + CONFIG.storage.expireMs).toString()); } function getSendHistory() { try { const raw = localStorage.getItem(LIMIT_KEY); if (!raw) return []; const arr = JSON.parse(raw); return Array.isArray(arr) ? arr : []; } catch (e) { console.warn('Failed to get sending history:', e); return []; } } function saveSendHistory(arr) { try { localStorage.setItem(LIMIT_KEY, JSON.stringify(arr)); } catch (e) { console.warn('Failed to save sending history:', e); } } function canSendNow() { const now = Date.now(); const history = getSendHistory().filter(ts => now - ts <= CONFIG.limits.rateWindowMs); saveSendHistory(history); return history.length < CONFIG.limits.rateLimit && !submitLock; } function recordSend() { const now = Date.now(); const history = getSendHistory().filter(ts => now - ts <= CONFIG.limits.rateWindowMs); history.push(now); saveSendHistory(history); } function appendMessage(text, from, save = true) { const cleanText = sanitizeInput(text); const wrapper = document.createElement("div"); wrapper.style.marginBottom = ".65rem"; wrapper.style.display = "flex"; wrapper.style.justifyContent = from === "user" ? "flex-end" : "flex-start"; const bubble = document.createElement("div"); bubble.style.maxWidth = "80%"; bubble.style.padding = ".45rem .7rem"; bubble.style.borderRadius = from === "user" ? "12px 12px 0 12px" : "12px 12px 12px 0"; bubble.style.background = from === "user" ? CONFIG.colors.primary : "#fff"; bubble.style.color = from === "user" ? "#fff" : "#333"; bubble.style.fontSize = ".85rem"; bubble.innerText = cleanText; bubble.setAttribute("chat-bubble", from); wrapper.appendChild(bubble); messages.appendChild(wrapper); scrollToBottom(); if (save) saveToCache(); } function appendHtmlMessage(html, from = "bot", save = true) { const cleanHtml = sanitizeHtmlForBot(html); const wrapper = document.createElement("div"); wrapper.style.marginBottom = ".65rem"; wrapper.style.display = "flex"; wrapper.style.justifyContent = from === "user" ? "flex-end" : "flex-start"; const bubble = document.createElement("div"); bubble.style.maxWidth = "80%"; bubble.style.padding = ".45rem .7rem"; bubble.style.borderRadius = from === "user" ? "12px 12px 0 12px" : "12px 12px 12px 0"; bubble.style.background = from === "user" ? CONFIG.colors.primary : "#fff"; bubble.style.color = from === "user" ? "#fff" : "#333"; bubble.style.fontSize = ".85rem"; bubble.setAttribute("chat-bubble", from); bubble.setAttribute("data-html", cleanHtml); bubble.innerHTML = cleanHtml; wrapper.appendChild(bubble); messages.appendChild(wrapper); scrollToBottom(); if (save) saveToCache(); } function appendCountdownMessage(initialSeconds) { if (countdownTimer) { clearInterval(countdownTimer); countdownTimer = null; } if (countdownBubble) { countdownBubble.remove(); countdownBubble = null; } let seconds; const storedTs = localStorage.getItem(COUNTDOWN_TS_KEY); if (typeof initialSeconds === "number") { seconds = initialSeconds; } else if (storedTs) { const start = Number(storedTs); if (!Number.isNaN(start)) { const elapsed = Math.floor((Date.now() - start) / 1000); seconds = 60 - elapsed; } } if (seconds == null) seconds = 60; if (seconds < 0) seconds = 0; const wrapper = document.createElement("div"); wrapper.style.marginBottom = ".65rem"; wrapper.style.display = "flex"; wrapper.style.justifyContent = "flex-start"; const bubble = document.createElement("div"); bubble.style.maxWidth = "80%"; bubble.style.padding = ".45rem .7rem"; bubble.style.borderRadius = "12px 12px 12px 0"; bubble.style.background = "#fff"; bubble.style.color = "#333"; bubble.style.fontSize = ".85rem"; bubble.style.boxShadow = "0 1px 3px rgba(0,0,0,.08)"; bubble.setAttribute("chat-bubble", "bot"); bubble.setAttribute("data-countdown", "1"); wrapper.appendChild(bubble); messages.appendChild(wrapper); countdownBubble = bubble; const setFinalText = () => { const finalHtml = 'If no reply received, email support@top10fans.us for assistance.'; countdownBubble.setAttribute("data-html", finalHtml); countdownBubble.removeAttribute("data-countdown"); countdownBubble.innerHTML = finalHtml; scrollToBottom(); saveToCache(); localStorage.removeItem(COUNTDOWN_TS_KEY); }; const updateText = () => { countdownBubble.textContent = `Sent. No reply in 1 minute? Email support@top10fans.us directly (${seconds}s)`; }; if (seconds <= 0) { setFinalText(); return; } updateText(); scrollToBottom(); saveToCache(); countdownTimer = setInterval(() => { seconds--; if (seconds <= 0) { clearInterval(countdownTimer); countdownTimer = null; setFinalText(); return; } updateText(); saveToCache(); }, 1000); } function clearChatHistory() { const messageBubbles = messages.querySelectorAll('div[chat-bubble]'); messageBubbles.forEach(bubble => { if (!bubble.closest('#lvga-privacy-tip')) { bubble.closest('div').remove(); } }); localStorage.removeItem(STORAGE_KEY); localStorage.removeItem(inputKey); localStorage.removeItem(COUNTDOWN_TS_KEY); localStorage.removeItem(STORAGE_EXPIRE_KEY); input.value = ''; updateWordCount(); appendMessage(CONFIG.texts.clearSuccess, "bot", false); } function showPrivacyTip() { const hasShown = localStorage.getItem(PRIVACY_SHOWN_KEY) === "1"; if (!hasShown && privacyTipElement) { privacyTipElement.style.display = "flex"; localStorage.setItem(PRIVACY_SHOWN_KEY, "1"); scrollToBottom(); } } let isOpen = localStorage.getItem(OPEN_KEY) === "1"; function adjustPanelHeight() { if (typeof isOpen !== 'undefined' && isOpen && panel) { const isMobile = window.innerWidth < 768; if (isMobile) { const panelHeight = window.innerHeight * 0.65; panel.style.maxHeight = `${panelHeight}px`; messages.style.maxHeight = `${panelHeight - 120}px`; } else { panel.style.maxHeight = '80vh'; messages.style.maxHeight = 'calc(80vh - 120px)'; panel.style.height = 'auto'; } } } try { loadFromCache(); if (isOpen) { panel.style.display = "flex"; btn.style.background = "#888"; iconChat.style.display = "none"; iconClose.style.display = "inline"; showPrivacyTip(); setTimeout(scrollToBottom, 0); adjustPanelHeight(); } else { panel.style.display = "none"; btn.style.background = CONFIG.colors.primary; iconChat.style.display = "inline-flex"; iconClose.style.display = "none"; } btn.addEventListener("click", () => { isOpen = !isOpen; panel.style.display = isOpen ? "flex" : "none"; btn.style.background = isOpen ? "#888" : CONFIG.colors.primary; iconChat.style.display = isOpen ? "none" : "inline-flex"; iconClose.style.display = isOpen ? "inline" : "none"; localStorage.setItem(OPEN_KEY, isOpen ? "1" : "0"); if (isOpen) { showPrivacyTip(); setTimeout(scrollToBottom, 0); updateWordCount(); adjustPanelHeight(); } }); btn.addEventListener("keydown", (e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); btn.click(); } }); iconClose.addEventListener("keydown", (e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); btn.click(); } }); clearHistoryBtn.addEventListener('click', () => { if (confirm(CONFIG.texts.clearConfirm)) { clearChatHistory(); } }); input.addEventListener("input", () => { updateWordCount(); if (!detectSensitiveInfo(input.value)) { localStorage.setItem(inputKey, input.value.trim()); } else { localStorage.removeItem(inputKey); } }); input.addEventListener("keydown", (e) => { const allowedKeys = ['Backspace', 'Delete', 'ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Tab', 'Meta', 'Control', 'Shift']; if (input.value.length >= CONFIG.limits.maxCharacters && !allowedKeys.includes(e.key) && !e.ctrlKey && !e.metaKey) { e.preventDefault(); } }); input.addEventListener("paste", (e) => { e.preventDefault(); const clipboardData = e.clipboardData || window.clipboardData; let pastedText = clipboardData.getData('text') || ''; if (detectSensitiveInfo(pastedText)) { sensitiveTipElement.style.display = 'block'; return; } const maxAdd = CONFIG.limits.maxCharacters - input.value.length; if (maxAdd <= 0) return; input.value += pastedText.substring(0, maxAdd); updateWordCount(); if (!detectSensitiveInfo(input.value)) { localStorage.setItem(inputKey, input.value.trim()); } }); form.addEventListener("submit", async function (e) { e.preventDefault(); let text = (input.value || "").trim(); text = sanitizeInput(text); if (text.length > CONFIG.limits.maxCharacters) { appendMessage(CONFIG.texts.overLimit.replace('{max}', CONFIG.limits.maxCharacters), "bot"); updateWordCount(); return; } if (detectSensitiveInfo(text)) { appendMessage(CONFIG.texts.sensitiveReject, "bot"); return; } if (!text) return input.focus(); if (!canSendNow()) { appendMessage(submitLock ? CONFIG.texts.repeatSubmit : CONFIG.texts.rateLimitTip, "bot"); return; } submitLock = true; submitBtn.disabled = true; recordSend(); appendMessage(text, "user"); input.value = ""; localStorage.removeItem(inputKey); updateWordCount(); submitBtn.style.opacity = "0.7"; const payload = { message: text, page: window.location.href, userAgent: navigator.userAgent, ts: new Date().toISOString(), lang: pageLang, }; try { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), CONFIG.api.timeout); const res = await fetch(CONFIG.api.endpoint, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), signal: controller.signal }); clearTimeout(timeoutId); if (res.status === 429) { try { const data = await res.json(); if (data && data.msg) { appendMessage(data.msg, "bot"); } else { appendMessage("Too many requests, please try again later", "bot"); } } catch (parseErr) { console.error("Failed to parse 429 response:", parseErr); appendMessage("Too many requests, please try again later", "bot"); } return; } if (!res.ok) throw new Error("HTTP " + res.status); const data = await res.json(); if (data && data.auto_reply_html) { appendHtmlMessage(data.auto_reply_html, "bot"); localStorage.removeItem(COUNTDOWN_TS_KEY); } else { const existingTs = localStorage.getItem(COUNTDOWN_TS_KEY); if (!existingTs) { localStorage.setItem(COUNTDOWN_TS_KEY, Date.now().toString()); appendCountdownMessage(); } else { if (!countdownTimer) { const start = Number(existingTs); if (!Number.isNaN(start)) { const elapsed = Math.floor((Date.now() - start) / 1000); const remain = 60 - elapsed; if (remain > 0) { appendCountdownMessage(remain); } else { appendCountdownMessage(0); } } } } } } catch (err) { console.error("Failed to send message:", err); const msg = (err && typeof err.message === "string") ? err.message : ""; if (err && err.name === "AbortError") { appendMessage("Request timed out, please check your network and try again", "bot"); } else if (msg.includes("HTTP")) { appendMessage(`Server error(${msg}), please try again later`, "bot"); } else { appendMessage(CONFIG.texts.sendFailed, "bot"); } } finally { setTimeout(() => { submitLock = false; submitBtn.disabled = false; submitBtn.style.opacity = "1"; }, 1500); } }); window.addEventListener('resize', adjustPanelHeight); updateWordCount(); } catch (err) { console.error("Chat component init failed:", err); const root = document.getElementById("lvga-chat-root"); if (root) { root.innerHTML = `<div style="padding: 1rem; text-align: center; font-family: system-ui;"> Customer service chat is temporarily down. Email support@top10fans.us for help. </div>`; } } })(); </script> </body> </html>