fix: Paywall-Killer sucht via iframe im Browser statt Server (429-Bypass)
This commit is contained in:
@@ -40,6 +40,7 @@ app.use((_req, res, next) => {
|
|||||||
"object-src 'none'",
|
"object-src 'none'",
|
||||||
"base-uri 'self'",
|
"base-uri 'self'",
|
||||||
"form-action 'self'",
|
"form-action 'self'",
|
||||||
|
"frame-src https://archive.ph https://archive.is https://archive.today",
|
||||||
].join('; '));
|
].join('; '));
|
||||||
res.setHeader('X-Content-Type-Options', 'nosniff');
|
res.setHeader('X-Content-Type-Options', 'nosniff');
|
||||||
res.setHeader('X-Frame-Options', 'DENY');
|
res.setHeader('X-Frame-Options', 'DENY');
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
import { useState, useRef } from 'react';
|
import { useState, useRef, useEffect } from 'react';
|
||||||
import { S, api } from '../lib.js';
|
import { S } from '../lib.js';
|
||||||
|
|
||||||
const C = {
|
const C = {
|
||||||
accent: '#f59e0b',
|
accent: '#f59e0b',
|
||||||
success: '#4ade80',
|
success: '#4ade80',
|
||||||
error: '#f87171',
|
error: '#f87171',
|
||||||
muted: '#6b7280',
|
muted: '#6b7280',
|
||||||
dim: '#1f2937',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function getDomain(url) {
|
function getDomain(url) {
|
||||||
@@ -14,34 +13,135 @@ function getDomain(url) {
|
|||||||
catch { return url; }
|
catch { return url; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Iframe-basierte Archivsuche (umgeht Server-IP-Block) ──────────────────
|
||||||
|
// Öffnet archive.ph/?url=... in einem versteckten iframe und liest
|
||||||
|
// nach dem Load die finale URL aus (Snapshot-Hash-URL).
|
||||||
|
function useArchiveSearch() {
|
||||||
|
const iframeRef = useRef(null);
|
||||||
|
const resolveRef = useRef(null);
|
||||||
|
const timeoutRef = useRef(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (timeoutRef.current) clearTimeout(timeoutRef.current);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
function search(targetUrl) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
resolveRef.current = { resolve, reject };
|
||||||
|
|
||||||
|
// Altes iframe entfernen
|
||||||
|
if (iframeRef.current) {
|
||||||
|
document.body.removeChild(iframeRef.current);
|
||||||
|
iframeRef.current = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const iframe = document.createElement('iframe');
|
||||||
|
iframe.style.cssText = 'position:fixed;top:-9999px;left:-9999px;width:1px;height:1px;opacity:0;pointer-events:none;border:none;';
|
||||||
|
iframe.sandbox = 'allow-scripts allow-same-origin allow-forms';
|
||||||
|
|
||||||
|
iframe.onload = () => {
|
||||||
|
if (timeoutRef.current) clearTimeout(timeoutRef.current);
|
||||||
|
try {
|
||||||
|
// Nach dem Load: finale URL des iframes prüfen
|
||||||
|
const finalUrl = iframe.contentWindow?.location?.href || '';
|
||||||
|
cleanup();
|
||||||
|
|
||||||
|
if (!finalUrl || finalUrl === 'about:blank') {
|
||||||
|
return resolve({ found: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Snapshot-URL hat Form https://archive.ph/HASH (4-10 Zeichen)
|
||||||
|
if (/archive\.(is|ph|today|fo|li|vn|md|gg)\/[a-zA-Z0-9]{4,10}$/.test(finalUrl)) {
|
||||||
|
return resolve({ found: true, url: finalUrl });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Suchergebnis-Seite geladen: Links nach Snapshots durchsuchen
|
||||||
|
try {
|
||||||
|
const doc = iframe.contentDocument || iframe.contentWindow?.document;
|
||||||
|
if (doc) {
|
||||||
|
// Alle Links durchsuchen
|
||||||
|
const links = Array.from(doc.querySelectorAll('a[href]'));
|
||||||
|
for (const a of links) {
|
||||||
|
const href = a.getAttribute('href') || '';
|
||||||
|
const abs = href.startsWith('http') ? href : `https://archive.ph${href}`;
|
||||||
|
if (/archive\.(is|ph|today|fo|li|vn|md|gg)\/[a-zA-Z0-9]{4,10}$/.test(abs)) {
|
||||||
|
const h = abs.split('/').pop();
|
||||||
|
const blacklist = ['newest','oldest','submit','search','about','rss','api','timemap'];
|
||||||
|
if (!blacklist.includes(h) && !/^\d+$/.test(h)) {
|
||||||
|
return resolve({ found: true, url: abs });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Cross-Origin Zugriff blockiert – trotzdem finalUrl prüfen
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve({ found: false });
|
||||||
|
} catch (e) {
|
||||||
|
cleanup();
|
||||||
|
resolve({ found: false });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
iframe.onerror = () => {
|
||||||
|
cleanup();
|
||||||
|
resolve({ found: false });
|
||||||
|
};
|
||||||
|
|
||||||
|
// Timeout nach 15s
|
||||||
|
timeoutRef.current = setTimeout(() => {
|
||||||
|
cleanup();
|
||||||
|
reject(new Error('Timeout beim Laden von archive.ph'));
|
||||||
|
}, 15000);
|
||||||
|
|
||||||
|
const searchUrl = `https://archive.ph/?url=${encodeURIComponent(targetUrl)}`;
|
||||||
|
iframe.src = searchUrl;
|
||||||
|
document.body.appendChild(iframe);
|
||||||
|
iframeRef.current = iframe;
|
||||||
|
|
||||||
|
function cleanup() {
|
||||||
|
if (timeoutRef.current) clearTimeout(timeoutRef.current);
|
||||||
|
if (iframeRef.current) {
|
||||||
|
try { document.body.removeChild(iframeRef.current); } catch {}
|
||||||
|
iframeRef.current = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return { search };
|
||||||
|
}
|
||||||
|
|
||||||
export default function PaywallKiller() {
|
export default function PaywallKiller() {
|
||||||
const [inputUrl, setInputUrl] = useState('');
|
const [inputUrl, setInputUrl] = useState('');
|
||||||
const [status, setStatus] = useState('idle');
|
const [status, setStatus] = useState('idle');
|
||||||
const [archiveUrl, setArchiveUrl] = useState('');
|
const [archiveUrl, setArchiveUrl] = useState('');
|
||||||
const [archiveType, setArchiveType] = useState('');
|
|
||||||
const [errMsg, setErrMsg] = useState('');
|
const [errMsg, setErrMsg] = useState('');
|
||||||
const [pdfLoading, setPdfLoading] = useState(false);
|
const [pdfLoading, setPdfLoading] = useState(false);
|
||||||
const inputRef = useRef(null);
|
const inputRef = useRef(null);
|
||||||
|
const { search } = useArchiveSearch();
|
||||||
|
|
||||||
async function handleCheck() {
|
async function handleCheck() {
|
||||||
const url = inputUrl.trim();
|
const url = inputUrl.trim();
|
||||||
if (!url) return;
|
if (!url) return;
|
||||||
|
let targetUrl = url.startsWith('http') ? url : 'https://' + url;
|
||||||
setStatus('checking');
|
setStatus('checking');
|
||||||
setErrMsg('');
|
setErrMsg('');
|
||||||
setArchiveUrl('');
|
setArchiveUrl('');
|
||||||
setArchiveType('');
|
|
||||||
try {
|
try {
|
||||||
const data = await api('/tools/paywallkiller/check', { body: { url } });
|
const result = await search(targetUrl);
|
||||||
setArchiveUrl(data.archiveUrl);
|
if (result.found) {
|
||||||
setArchiveType(data.type);
|
setArchiveUrl(result.url);
|
||||||
setStatus('found');
|
setStatus('found');
|
||||||
} catch (err) {
|
|
||||||
if (err.message && err.message.includes('Kein Archiv')) {
|
|
||||||
setStatus('notfound');
|
|
||||||
} else {
|
} else {
|
||||||
setErrMsg(err.message || 'Unbekannter Fehler');
|
setStatus('notfound');
|
||||||
setStatus('error');
|
|
||||||
}
|
}
|
||||||
|
} catch (err) {
|
||||||
|
setErrMsg(err.message || 'Fehler beim Suchen');
|
||||||
|
setStatus('error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,7 +182,6 @@ export default function PaywallKiller() {
|
|||||||
setInputUrl('');
|
setInputUrl('');
|
||||||
setStatus('idle');
|
setStatus('idle');
|
||||||
setArchiveUrl('');
|
setArchiveUrl('');
|
||||||
setArchiveType('');
|
|
||||||
setErrMsg('');
|
setErrMsg('');
|
||||||
setPdfLoading(false);
|
setPdfLoading(false);
|
||||||
setTimeout(() => inputRef.current?.focus(), 50);
|
setTimeout(() => inputRef.current?.focus(), 50);
|
||||||
@@ -92,9 +191,7 @@ export default function PaywallKiller() {
|
|||||||
try {
|
try {
|
||||||
const text = await navigator.clipboard.readText();
|
const text = await navigator.clipboard.readText();
|
||||||
if (text) setInputUrl(text.trim());
|
if (text) setInputUrl(text.trim());
|
||||||
} catch {
|
} catch { inputRef.current?.focus(); }
|
||||||
inputRef.current?.focus();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const isChecking = status === 'checking';
|
const isChecking = status === 'checking';
|
||||||
@@ -106,24 +203,18 @@ export default function PaywallKiller() {
|
|||||||
return (
|
return (
|
||||||
<div style={{ padding: '20px 16px', maxWidth: 640, margin: '0 auto' }}>
|
<div style={{ padding: '20px 16px', maxWidth: 640, margin: '0 auto' }}>
|
||||||
|
|
||||||
{/* Header */}
|
|
||||||
<div style={{ marginBottom: 24 }}>
|
<div style={{ marginBottom: 24 }}>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 4 }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 4 }}>
|
||||||
<span style={{ fontSize: 20 }}>🔓</span>
|
<span style={{ fontSize: 20 }}>🔓</span>
|
||||||
<span style={{ color: C.accent, fontFamily: 'monospace', fontSize: 13, letterSpacing: 1 }}>
|
<span style={{ color: C.accent, fontFamily: 'monospace', fontSize: 13, letterSpacing: 1 }}>PAYWALL-KILLER</span>
|
||||||
PAYWALL-KILLER
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div style={{ color: C.muted, fontSize: 11, fontFamily: 'monospace' }}>
|
<div style={{ color: C.muted, fontSize: 11, fontFamily: 'monospace' }}>
|
||||||
Artikel über archive.is abrufen · Als PDF speichern
|
Artikel über archive.ph abrufen · Als PDF speichern
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Eingabe */}
|
|
||||||
<div style={{ ...S.card, marginBottom: 16 }}>
|
<div style={{ ...S.card, marginBottom: 16 }}>
|
||||||
<div style={{ ...S.head, marginBottom: 10 }}>ARTIKEL-URL</div>
|
<div style={{ ...S.head, marginBottom: 10 }}>ARTIKEL-URL</div>
|
||||||
|
|
||||||
{/* Input + Paste-Button */}
|
|
||||||
<div style={{ display: 'flex', gap: 8, marginBottom: 10 }}>
|
<div style={{ display: 'flex', gap: 8, marginBottom: 10 }}>
|
||||||
<input
|
<input
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
@@ -133,178 +224,93 @@ export default function PaywallKiller() {
|
|||||||
placeholder="https://www.zeitung.de/artikel/..."
|
placeholder="https://www.zeitung.de/artikel/..."
|
||||||
style={{ ...S.inp, flex: 1 }}
|
style={{ ...S.inp, flex: 1 }}
|
||||||
/>
|
/>
|
||||||
<button
|
<button onClick={handlePaste} title="Aus Zwischenablage"
|
||||||
onClick={handlePaste}
|
style={{ background: 'rgba(255,255,255,0.06)', border: '1px solid rgba(255,255,255,0.12)', borderRadius: 6, padding: '0 12px', color: '#fff', cursor: 'pointer', fontSize: 14, flexShrink: 0 }}>
|
||||||
title="Aus Zwischenablage einfügen"
|
|
||||||
style={{
|
|
||||||
background: 'rgba(255,255,255,0.06)',
|
|
||||||
border: '1px solid rgba(255,255,255,0.12)',
|
|
||||||
borderRadius: 6,
|
|
||||||
padding: '0 12px',
|
|
||||||
color: '#fff',
|
|
||||||
cursor: 'pointer',
|
|
||||||
fontSize: 14,
|
|
||||||
flexShrink: 0,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
📋
|
📋
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Action-Buttons */}
|
|
||||||
<div style={{ display: 'flex', gap: 8 }}>
|
<div style={{ display: 'flex', gap: 8 }}>
|
||||||
<button
|
<button onClick={handleCheck} disabled={isChecking || !inputUrl.trim()}
|
||||||
onClick={handleCheck}
|
style={{ ...S.btn(C.accent), flex: 1, opacity: (isChecking || !inputUrl.trim()) ? 0.45 : 1, cursor: (isChecking || !inputUrl.trim()) ? 'default' : 'pointer' }}>
|
||||||
disabled={isChecking || !inputUrl.trim()}
|
{isChecking ? '⏳ Lade archive.ph...' : '🔍 Archiv suchen'}
|
||||||
style={{
|
|
||||||
...S.btn(C.accent),
|
|
||||||
flex: 1,
|
|
||||||
opacity: (isChecking || !inputUrl.trim()) ? 0.45 : 1,
|
|
||||||
cursor: (isChecking || !inputUrl.trim()) ? 'default' : 'pointer',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{isChecking ? '⏳ Suche Archiv...' : '🔍 Archiv suchen'}
|
|
||||||
</button>
|
</button>
|
||||||
{hasResult && (
|
{hasResult && (
|
||||||
<button
|
<button onClick={handleReset}
|
||||||
onClick={handleReset}
|
style={{ background: 'rgba(255,255,255,0.06)', border: '1px solid rgba(255,255,255,0.14)', borderRadius: 6, padding: '8px 14px', color: 'rgba(255,255,255,0.6)', cursor: 'pointer', fontFamily: 'monospace', fontSize: 12, whiteSpace: 'nowrap' }}>
|
||||||
style={{
|
|
||||||
background: 'rgba(255,255,255,0.06)',
|
|
||||||
border: '1px solid rgba(255,255,255,0.14)',
|
|
||||||
borderRadius: 6,
|
|
||||||
padding: '8px 14px',
|
|
||||||
color: 'rgba(255,255,255,0.6)',
|
|
||||||
cursor: 'pointer',
|
|
||||||
fontFamily: 'monospace',
|
|
||||||
fontSize: 12,
|
|
||||||
whiteSpace: 'nowrap',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
✕ Reset
|
✕ Reset
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
{isChecking && (
|
||||||
|
<div style={{ color: C.muted, fontSize: 11, fontFamily: 'monospace', marginTop: 10 }}>
|
||||||
|
archive.ph wird direkt im Browser abgefragt...
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Gefunden */}
|
|
||||||
{isFound && (
|
{isFound && (
|
||||||
<div style={{ ...S.card, borderColor: `${C.success}55`, marginBottom: 16 }}>
|
<div style={{ ...S.card, borderColor: `${C.success}55`, marginBottom: 16 }}>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 12 }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 12 }}>
|
||||||
<span>✅</span>
|
<span>✅</span>
|
||||||
<span style={{ color: C.success, fontFamily: 'monospace', fontSize: 12 }}>
|
<span style={{ color: C.success, fontFamily: 'monospace', fontSize: 12 }}>Archiv gefunden</span>
|
||||||
Archiv gefunden
|
|
||||||
</span>
|
|
||||||
<span style={{
|
|
||||||
background: `${C.accent}20`,
|
|
||||||
border: `1px solid ${C.accent}55`,
|
|
||||||
borderRadius: 4,
|
|
||||||
padding: '1px 7px',
|
|
||||||
fontSize: 10,
|
|
||||||
color: C.accent,
|
|
||||||
fontFamily: 'monospace',
|
|
||||||
}}>
|
|
||||||
{archiveType === 'newest' ? 'neuester Snapshot' : 'ältester Snapshot'}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div style={{ background: 'rgba(255,255,255,0.03)', border: '1px solid rgba(255,255,255,0.08)', borderRadius: 6, padding: '8px 10px', marginBottom: 14, wordBreak: 'break-all' }}>
|
||||||
<div style={{
|
<a href={archiveUrl} target="_blank" rel="noreferrer" style={{ color: C.accent, fontSize: 11, fontFamily: 'monospace' }}>
|
||||||
background: 'rgba(255,255,255,0.03)',
|
|
||||||
border: '1px solid rgba(255,255,255,0.08)',
|
|
||||||
borderRadius: 6,
|
|
||||||
padding: '8px 10px',
|
|
||||||
marginBottom: 14,
|
|
||||||
wordBreak: 'break-all',
|
|
||||||
}}>
|
|
||||||
<a href={archiveUrl} target="_blank" rel="noreferrer"
|
|
||||||
style={{ color: C.accent, fontSize: 11, fontFamily: 'monospace' }}>
|
|
||||||
{archiveUrl}
|
{archiveUrl}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
|
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
|
||||||
<a
|
<a href={archiveUrl} target="_blank" rel="noreferrer" style={{ ...S.btn(C.success), textDecoration: 'none' }}>
|
||||||
href={archiveUrl}
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
style={{ ...S.btn(C.success), textDecoration: 'none' }}
|
|
||||||
>
|
|
||||||
🌐 Im Browser öffnen
|
🌐 Im Browser öffnen
|
||||||
</a>
|
</a>
|
||||||
<button
|
<button onClick={handlePdf} disabled={pdfLoading}
|
||||||
onClick={handlePdf}
|
style={{ ...S.btn(C.accent), opacity: pdfLoading ? 0.5 : 1, cursor: pdfLoading ? 'default' : 'pointer' }}>
|
||||||
disabled={pdfLoading}
|
|
||||||
style={{
|
|
||||||
...S.btn(C.accent),
|
|
||||||
opacity: pdfLoading ? 0.5 : 1,
|
|
||||||
cursor: pdfLoading ? 'default' : 'pointer',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{pdfLoading ? '⏳ Erstelle PDF...' : '⬇ Als PDF speichern'}
|
{pdfLoading ? '⏳ Erstelle PDF...' : '⬇ Als PDF speichern'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{pdfLoading && (
|
{pdfLoading && (
|
||||||
<div style={{ color: C.muted, fontSize: 11, fontFamily: 'monospace', marginTop: 10 }}>
|
<div style={{ color: C.muted, fontSize: 11, fontFamily: 'monospace', marginTop: 10 }}>
|
||||||
Seite wird gerendert, das dauert 15–30 Sekunden...
|
Seite wird serverseitig gerendert, dauert 15–30 Sekunden...
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Kein Archiv */}
|
|
||||||
{isNotFound && (
|
{isNotFound && (
|
||||||
<div style={{ ...S.card, borderColor: `${C.error}44` }}>
|
<div style={{ ...S.card, borderColor: `${C.error}44` }}>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 8 }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 8 }}>
|
||||||
<span>📭</span>
|
<span>📭</span>
|
||||||
<span style={{ color: C.error, fontFamily: 'monospace', fontSize: 12 }}>
|
<span style={{ color: C.error, fontFamily: 'monospace', fontSize: 12 }}>Kein Archiv gefunden</span>
|
||||||
Kein Archiv gefunden
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div style={{ color: C.muted, fontSize: 11, fontFamily: 'monospace', lineHeight: 1.6, marginBottom: 12 }}>
|
<div style={{ color: C.muted, fontSize: 11, fontFamily: 'monospace', lineHeight: 1.6, marginBottom: 12 }}>
|
||||||
Für <strong style={{ color: 'rgba(255,255,255,0.55)' }}>{getDomain(inputUrl)}</strong> wurde
|
Für <strong style={{ color: 'rgba(255,255,255,0.55)' }}>{getDomain(inputUrl)}</strong> wurde kein Snapshot auf archive.ph gefunden.
|
||||||
kein Snapshot auf archive.is gefunden.
|
|
||||||
</div>
|
</div>
|
||||||
<a
|
<a href={`https://archive.ph/${inputUrl.trim()}`} target="_blank" rel="noreferrer"
|
||||||
href={`https://archive.is/${inputUrl.trim()}`}
|
style={{ ...S.btn(C.muted), textDecoration: 'none', display: 'inline-block', fontSize: 11 }}>
|
||||||
target="_blank"
|
🔗 Manuell auf archive.ph prüfen
|
||||||
rel="noreferrer"
|
|
||||||
style={{ ...S.btn(C.muted), textDecoration: 'none', display: 'inline-block', fontSize: 11 }}
|
|
||||||
>
|
|
||||||
🔗 Manuell auf archive.is prüfen
|
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Fehler */}
|
|
||||||
{isError && (
|
{isError && (
|
||||||
<div style={{ ...S.card, borderColor: `${C.error}44` }}>
|
<div style={{ ...S.card, borderColor: `${C.error}44` }}>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||||
<span>⚠️</span>
|
<span>⚠️</span>
|
||||||
<span style={{ color: C.error, fontFamily: 'monospace', fontSize: 12 }}>
|
<span style={{ color: C.error, fontFamily: 'monospace', fontSize: 12 }}>{errMsg}</span>
|
||||||
{errMsg}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Idle-Info */}
|
|
||||||
{status === 'idle' && (
|
{status === 'idle' && (
|
||||||
<div style={{ ...S.card }}>
|
<div style={{ ...S.card }}>
|
||||||
<div style={{ ...S.head, marginBottom: 10 }}>SO FUNKTIONIERT ES</div>
|
<div style={{ ...S.head, marginBottom: 10 }}>SO FUNKTIONIERT ES</div>
|
||||||
{[
|
{[
|
||||||
['1', 'URL des gesperrten Artikels einfügen'],
|
['1', 'URL des gesperrten Artikels einfügen'],
|
||||||
['2', 'Archiv auf archive.is suchen'],
|
['2', 'Archiv auf archive.ph suchen (direkt im Browser)'],
|
||||||
['3', 'Im Browser öffnen oder als PDF speichern'],
|
['3', 'Im Browser öffnen oder als PDF speichern'],
|
||||||
].map(([num, text]) => (
|
].map(([num, text]) => (
|
||||||
<div key={num} style={{ display: 'flex', gap: 10, alignItems: 'center', marginBottom: 8 }}>
|
<div key={num} style={{ display: 'flex', gap: 10, alignItems: 'center', marginBottom: 8 }}>
|
||||||
<span style={{
|
<span style={{ background: `${C.accent}20`, border: `1px solid ${C.accent}44`, borderRadius: '50%', width: 20, height: 20, flexShrink: 0, display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 10, color: C.accent, fontFamily: 'monospace' }}>{num}</span>
|
||||||
background: `${C.accent}20`,
|
|
||||||
border: `1px solid ${C.accent}44`,
|
|
||||||
borderRadius: '50%',
|
|
||||||
width: 20, height: 20, flexShrink: 0,
|
|
||||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
||||||
fontSize: 10, color: C.accent, fontFamily: 'monospace',
|
|
||||||
}}>{num}</span>
|
|
||||||
<span style={{ color: C.muted, fontSize: 11, fontFamily: 'monospace' }}>{text}</span>
|
<span style={{ color: C.muted, fontSize: 11, fontFamily: 'monospace' }}>{text}</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|||||||
Reference in New Issue
Block a user