Was ist PixelDrop?
PixelDrop, der Titel dieses Scriptes, ist ein selbst gehosteter Bildhosting Dienst auf Basis von PHP. Nutzer laden Bilder hoch und erhalten sofort einen teilbaren Link sowie einen Direktlink zur Bilddatei. Kein Login, keine externe Datenbank, kein Setup Wizard, einfach hochladen und fertig. Das Script ist dahingehend absolut einfach gehalten.
Alle Features im Überblick:
- Single File Architektur: Das gesamte Script inklusive HTML, CSS, JavaScript und PHP Backend besteht aus einer einzigen Datei.
- Auto Setup: Beim ersten Aufruf erstellt das Script automatisch alle benötigten Verzeichnisse, .htaccess-Dateien, robots.txt und die SQLite Datenbank.
- Auto Delete: Bilder werden nach konfigurierbaren 7 Tagen automatisch vom Server gelöscht, ohne Cronjob, via probabilistischem Cleanup.
- Drag & Drop Upload: Modernes Upload Interface mit Dateivorschau, Fortschrittsanzeige und sofortigem Link.
- Zwei Link-Typen: Share Link (kurze URL) und Direktlink (zur Bilddatei) werden nach dem Upload angezeigt.
- Sicherheit: CSRF Token Validierung, serverseitige MIME Type Prüfung (nicht nur Extension), Rate Limiting (30 Uploads/Stunde pro IP), gesicherte Upload Verzeichnisse per .htaccess.
- SQLite Datenbank: Leichtgewichtige Datenbank ohne externen Datenbankserver. Speichert Token, Dateiname, Ablaufdatum, IP-Hash und View Counter.
- View-Counter: Jede Bildansichtsseite zählt Aufrufe und zeigt Metadaten (Dateigröße, Abmessungen, Ablaufdatum).
- Modernes Design: Atelier Ästhetik mit Cormorant Garamond & DM Sans, Gold Akzenten, Grain Overlay und smooth Animationen.
- Responsive: Vollständig mobiloptimiert für alle Bildschirmgrößen.
- OG Tags: Automatische Open Graph Meta Tags für ansprechende Link Vorschauen in Chat Apps und sozialen Netzwerken.
- Keine Abhängigkeiten: Kein Composer, kein npm, kein Framework.
<?php
/**
* PixelDrop - Image Upload Service
* Powered by Dreamcodes.net
*/
define('APP_VERSION', '1.0.0');
define('BASE_DIR', __DIR__);
define('UPLOAD_DIR', BASE_DIR . '/uploads');
define('DB_FILE', BASE_DIR . '/data/pixeldrop.db');
define('DATA_DIR', BASE_DIR . '/data');
define('MAX_FILE_SIZE', 10 * 1024 * 1024); // 10MB
define('ALLOWED_TYPES', ['image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/svg+xml']);
define('ALLOWED_EXTENSIONS', ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg']);
define('EXPIRY_DAYS', 7);
define('SITE_URL', (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https' : 'http') . '://' . ($_SERVER['HTTP_HOST'] ?? 'localhost'));
function wf($path, $content) { if (!file_exists($path)) file_put_contents($path, $content); }
function setup() {
foreach ([UPLOAD_DIR, DATA_DIR] as $dir) if (!is_dir($dir)) mkdir($dir, 0755, true);
wf(BASE_DIR.'/.htaccess', "Options -Indexes\nRewriteEngine On\nRewriteCond %{REQUEST_FILENAME} !-f\nRewriteCond %{REQUEST_FILENAME} !-d\nRewriteRule ^([a-zA-Z0-9_-]+)$ index.php?view=$1 [L,QSA]\nRewriteRule ^([a-zA-Z0-9_-]+)\\.([a-zA-Z0-9]+)$ index.php?view=$1 [L,QSA]\n");
wf(UPLOAD_DIR.'/.htaccess', "Options -Indexes\nOptions -ExecCGI\nAddHandler cgi-script .php .php3 .php4 .php5 .phtml .pl .py .jsp .asp .htm .shtml .sh .cgi\nphp_flag engine off\n<FilesMatch \"\\.(php|php3|php4|php5|phtml|pl|py|jsp|asp|sh|cgi)$\">\n Deny from all\n</FilesMatch>\n");
wf(DATA_DIR.'/.htaccess', "Deny from all\n");
wf(BASE_DIR.'/robots.txt', "User-agent: *\nDisallow: /uploads/\nDisallow: /data/\n");
initDB();
}
function initDB() { $pdo=getDB(); $pdo->exec("CREATE TABLE IF NOT EXISTS images(id INTEGER PRIMARY KEY AUTOINCREMENT,token TEXT UNIQUE NOT NULL,filename TEXT NOT NULL,original_name TEXT NOT NULL,mime_type TEXT NOT NULL,file_size INTEGER NOT NULL,width INTEGER DEFAULT 0,height INTEGER DEFAULT 0,uploaded_at INTEGER NOT NULL,expires_at INTEGER NOT NULL,ip_hash TEXT,views INTEGER DEFAULT 0);CREATE INDEX IF NOT EXISTS idx_token ON images(token);CREATE INDEX IF NOT EXISTS idx_expires ON images(expires_at);"); }
function getDB() { static $pdo=null; if($pdo===null){ $pdo=new PDO('sqlite:'.DB_FILE,null,null,[PDO::ATTR_ERRMODE=>PDO::ERRMODE_EXCEPTION,PDO::ATTR_DEFAULT_FETCH_MODE=>PDO::FETCH_ASSOC]); $pdo->exec("PRAGMA journal_mode=WAL;PRAGMA synchronous=NORMAL;"); } return $pdo; }
function cleanupExpired() {
if (rand(1, 10) !== 1) return;
$pdo = getDB();
$expired = $pdo->query("SELECT filename FROM images WHERE expires_at < " . time())->fetchAll();
foreach ($expired as $img) {
$path = UPLOAD_DIR . '/' . $img['filename'];
if (file_exists($path)) unlink($path);
}
$pdo->exec("DELETE FROM images WHERE expires_at < " . time());
}
function generateToken($length=10) { return bin2hex(random_bytes($length)); }
function sanitizeFilename($n){$n=preg_replace('/[^a-zA-Z0-9._-]/','_',$n);$n=preg_replace('/\.{2,}/','.',$n);return substr($n,0,100);}
function hashIp($ip) { return hash('sha256', $ip.'pixeldrop_salt_2024'); }
function rateLimitCheck($h){$c=getDB()->prepare("SELECT COUNT(*) FROM images WHERE ip_hash=? AND uploaded_at>?");$c->execute([$h,time()-3600]);return $c->fetchColumn()<30;}
function getImageDimensions($p,$m){if(function_exists('getimagesize')&&in_array($m,['image/jpeg','image/png','image/gif','image/webp'])){$s=@getimagesize($p);if($s)return[$s[0],$s[1]];}return[0,0];}
function handleUpload() {
header('Content-Type: application/json');
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['error' => 'Methode nicht erlaubt']);
exit;
}
if (empty($_POST['csrf']) || $_POST['csrf'] !== ($_SESSION['csrf'] ?? '')) {
http_response_code(403);
echo json_encode(['error' => 'Ungültige Anfrage']);
exit;
}
if (empty($_FILES['image'])) {
http_response_code(400);
echo json_encode(['error' => 'Keine Datei empfangen']);
exit;
}
$file = $_FILES['image'];
if ($file['error'] !== UPLOAD_ERR_OK) {
$errors = [
UPLOAD_ERR_INI_SIZE => 'Datei zu groß (Server-Limit)',
UPLOAD_ERR_FORM_SIZE => 'Datei zu groß',
UPLOAD_ERR_PARTIAL => 'Upload unvollständig',
UPLOAD_ERR_NO_FILE => 'Keine Datei ausgewählt',
];
http_response_code(400);
echo json_encode(['error' => $errors[$file['error']] ?? 'Upload-Fehler']);
exit;
}
if ($file['size'] > MAX_FILE_SIZE) {
http_response_code(400);
echo json_encode(['error' => 'Datei zu groß. Maximum: 10 MB']);
exit;
}
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mime = $finfo->file($file['tmp_name']);
if (!in_array($mime, ALLOWED_TYPES)) {
http_response_code(400);
echo json_encode(['error' => 'Dateityp nicht erlaubt. Nur JPG, PNG, GIF, WebP, SVG']);
exit;
}
$ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
if (!in_array($ext, ALLOWED_EXTENSIONS)) {
http_response_code(400);
echo json_encode(['error' => 'Ungültige Dateiendung']);
exit;
}
$ip_hash = hashIp($_SERVER['REMOTE_ADDR'] ?? '0.0.0.0');
if (!rateLimitCheck($ip_hash)) {
http_response_code(429);
echo json_encode(['error' => 'Zu viele Uploads. Bitte warte eine Stunde.']);
exit;
}
$token = generateToken(8);
$safe_ext = ($mime === 'image/svg+xml') ? 'svg' : $ext;
$filename = $token . '_' . time() . '.' . $safe_ext;
$dest = UPLOAD_DIR . '/' . $filename;
if (!move_uploaded_file($file['tmp_name'], $dest)) {
http_response_code(500);
echo json_encode(['error' => 'Fehler beim Speichern der Datei']);
exit;
}
[$w, $h] = getImageDimensions($dest, $mime);
$pdo = getDB();
$stmt = $pdo->prepare("
INSERT INTO images (token, filename, original_name, mime_type, file_size, width, height, uploaded_at, expires_at, ip_hash)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
");
$stmt->execute([
$token,
$filename,
sanitizeFilename($file['name']),
$mime,
$file['size'],
$w, $h,
time(),
time() + (EXPIRY_DAYS * 86400),
$ip_hash
]);
$url = SITE_URL . '/' . $token;
$direct = SITE_URL . '/uploads/' . $filename;
echo json_encode([
'success' => true,
'token' => $token,
'url' => $url,
'direct' => $direct,
'expires' => date('d.m.Y', time() + (EXPIRY_DAYS * 86400)),
'size' => formatBytes($file['size']),
'dimensions' => $w && $h ? $w . '×' . $h . 'px' : null,
]);
exit;
}
function handleView($token) {
$token = preg_replace('/[^a-zA-Z0-9]/', '', $token);
$pdo = getDB();
$stmt = $pdo->prepare("SELECT * FROM images WHERE token = ? AND expires_at > ?");
$stmt->execute([$token, time()]);
$img = $stmt->fetch();
if (!$img) return null;
$pdo->prepare("UPDATE images SET views = views + 1 WHERE token = ?")->execute([$token]);
return $img;
}
function formatBytes($b){if($b>=1048576)return round($b/1048576,1).' MB';if($b>=1024)return round($b/1024,1).' KB';return $b.' B';}
function timeLeft($e){$d=floor(($e-time())/86400);$h=floor((($e-time())%86400)/3600);if($d>0)return $d.' Tag'.($d!==1?'e':'');if($h>0)return $h.' Stunde'.($h!==1?'n':'');return 'weniger als 1 Stunde';}
setup(); cleanupExpired(); session_start();
if(empty($_SESSION['csrf'])) $_SESSION['csrf']=bin2hex(random_bytes(16));
$action=$_GET['action']??''; $view_token=$_GET['view']??'';
if($action==='upload'){handleUpload();exit;}
$view_data=$view_token?handleView($view_token):null;
?>
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?= $view_data ? htmlspecialchars($view_data['original_name']) . ' — PixelDrop' : 'PixelDrop — Bildhosting' ?></title>
<meta name="description" content="Schnelles, sicheres Bildhosting. Lade Bilder hoch und teile den Link – automatisch nach 7 Tagen gelöscht.">
<meta name="robots" content="<?= $view_data ? 'noindex' : 'index,follow' ?>">
<?php if ($view_data): ?>
<meta property="og:image" content="<?= SITE_URL . '/uploads/' . htmlspecialchars($view_data['filename']) ?>">
<meta property="og:title" content="<?= htmlspecialchars($view_data['original_name']) ?>">
<?php endif; ?>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,300;0,400;0,600;1,300;1,400&family=DM+Sans:opsz,wght@9..40,300;9..40,400;9..40,500&display=swap" rel="stylesheet">
<style>
:root { --ink:#0e0e0f; --ink-soft:#3a3a3c; --ink-muted:#7a7a80; --canvas:#f7f5f2; --canvas-2:#eeece8; --canvas-3:#e4e1db; --gold:#b8965a; --gold-light:#d4b07a; --gold-dark:#8c6d3a; --white:#ffffff; --danger:#c0392b; --success:#2e7d52; --font-display:'Cormorant Garamond', Georgia, serif; --font-body:'DM Sans', system-ui, sans-serif; --radius-sm:4px; --radius:8px; --radius-lg:16px; --shadow-sm:0 1px 3px rgba(14,14,15,.06), 0 1px 2px rgba(14,14,15,.08); --shadow:0 4px 16px rgba(14,14,15,.08), 0 1px 4px rgba(14,14,15,.06); --shadow-lg:0 16px 48px rgba(14,14,15,.12), 0 4px 16px rgba(14,14,15,.08); --shadow-xl:0 32px 80px rgba(14,14,15,.15), 0 8px 24px rgba(14,14,15,.10) }
*, *::before, *::after { box-sizing:border-box; margin:0; padding:0 }
html { scroll-behavior:smooth; -webkit-font-smoothing:antialiased }
body { font-family:var(--font-body); font-size:15px; line-height:1.6; color:var(--ink); background:var(--canvas); min-height:100vh; display:flex; flex-direction:column }
body::before { content:''; position:fixed; inset:0; background-image:url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)' opacity='0.03'/%3E%3C/svg%3E"); pointer-events:none; z-index:9999; opacity:.4 }
.header { padding:0 max(2rem, calc(50% - 38rem)); height:72px; display:flex; align-items:center; justify-content:space-between; border-bottom:1px solid var(--canvas-3); background:rgba(247,245,242,.92); backdrop-filter:blur(12px); position:sticky; top:0; z-index:100 }
.logo { display:flex; align-items:baseline; gap:.35rem; text-decoration:none; color:var(--ink) }
.logo-mark { font-family:var(--font-display); font-size:1.6rem; font-weight:600; letter-spacing:-.02em; line-height:1 }
.logo-dot { width:5px; height:5px; background:var(--gold); border-radius:50%; margin-bottom:.25rem; flex-shrink:0 }
.logo-sub { font-size:.65rem; font-weight:500; letter-spacing:.18em; text-transform:uppercase; color:var(--ink-muted); margin-left:.1rem }
.header-badge { font-size:.72rem; font-weight:500; letter-spacing:.1em; text-transform:uppercase; color:var(--ink-muted); border:1px solid var(--canvas-3); padding:.25rem .75rem; border-radius:20px }
main { flex:1; padding:4rem max(2rem, calc(50% - 38rem)) }
.hero { text-align:center; margin-bottom:4rem; animation:fadeUp .8s ease both }
.hero-eyebrow { font-size:.7rem; font-weight:500; letter-spacing:.25em; text-transform:uppercase; color:var(--gold); margin-bottom:1.25rem; display:flex; align-items:center; justify-content:center; gap:.75rem }
.hero-eyebrow::before, .hero-eyebrow::after { content:''; flex:1; max-width:3rem; height:1px; background:linear-gradient(90deg, transparent, var(--gold-light)) }
.hero-eyebrow::after { background:linear-gradient(90deg, var(--gold-light), transparent) }
.hero h1 { font-family:var(--font-display); font-size:clamp(2.8rem, 6vw, 4.5rem); font-weight:300; line-height:1.08; letter-spacing:-.02em; color:var(--ink); margin-bottom:.75rem }
.hero h1 em { font-style:italic; color:var(--gold) }
.hero p { font-size:1rem; color:var(--ink-muted); max-width:36rem; margin:0 auto; font-weight:300; line-height:1.8 }
.upload-card { background:var(--white); border:1px solid var(--canvas-3); border-radius:var(--radius-lg); box-shadow:var(--shadow-lg); padding:3rem; max-width:600px; margin:0 auto; animation:fadeUp .8s ease .15s both; position:relative; overflow:hidden }
.upload-card::before { content:''; position:absolute; top:0; left:0; right:0; height:2px; background:linear-gradient(90deg, transparent, var(--gold-light), var(--gold), var(--gold-light), transparent) }
.dropzone { border:2px dashed var(--canvas-3); border-radius:var(--radius); padding:3.5rem 2rem; text-align:center; cursor:pointer; transition:all .25s ease; position:relative; background:var(--canvas) }
.dropzone:hover, .dropzone.dragover { border-color:var(--gold-light); background:rgba(184,150,90,.04) }
.dropzone input[type=file] { position:absolute; inset:0; opacity:0; cursor:pointer; width:100%; height:100% }
.drop-icon { width:52px; height:52px; margin:0 auto 1.25rem; display:flex; align-items:center; justify-content:center; background:var(--canvas-2); border-radius:50%; transition:transform .3s ease, background .25s }
.dropzone:hover .drop-icon { transform:translateY(-3px); background:rgba(184,150,90,.12) }
.drop-icon svg { width:22px; height:22px; stroke:var(--gold) }
.drop-title { font-family:var(--font-display); font-size:1.3rem; font-weight:400; color:var(--ink); margin-bottom:.4rem }
.drop-sub { font-size:.82rem; color:var(--ink-muted); font-weight:300 }
.drop-sub strong { color:var(--ink-soft); font-weight:500 }
.preview-wrap { display:none; position:relative }
.preview-wrap.show { display:block }
.preview-img { width:100%; max-height:280px; object-fit:contain; border-radius:var(--radius); background:var(--canvas) }
.preview-remove { position:absolute; top:.6rem; right:.6rem; width:28px; height:28px; background:rgba(14,14,15,.6); border:none; border-radius:50%; cursor:pointer; display:flex; align-items:center; justify-content:center; transition:background .2s }
.preview-remove:hover { background:rgba(192,57,43,.85) }
.preview-remove svg { width:12px; height:12px; stroke:#fff; stroke-width:2.5 }
.preview-info { margin-top:.75rem; font-size:.8rem; color:var(--ink-muted); display:flex; gap:1rem }
.preview-info span::before { content:'·'; margin-right:.5rem; color:var(--gold) }
.progress-wrap { display:none; margin-top:1.25rem }
.progress-wrap.show { display:block }
.progress-bar-track { height:3px; background:var(--canvas-2); border-radius:2px; overflow:hidden }
.progress-bar-fill { height:100%; background:linear-gradient(90deg, var(--gold-dark), var(--gold), var(--gold-light)); border-radius:2px; width:0; transition:width .2s ease }
.progress-label { font-size:.75rem; color:var(--ink-muted); text-align:center; margin-top:.5rem }
.btn { display:inline-flex; align-items:center; justify-content:center; gap:.5rem; padding:.875rem 2rem; font-family:var(--font-body); font-size:.85rem; font-weight:500; letter-spacing:.06em; text-transform:uppercase; border:none; border-radius:var(--radius-sm); cursor:pointer; transition:all .2s ease; text-decoration:none }
.btn-gold { background:linear-gradient(135deg, var(--gold-dark), var(--gold)); color:var(--white); box-shadow:0 2px 12px rgba(184,150,90,.3) }
.btn-gold:hover:not(:disabled) { transform:translateY(-1px); box-shadow:0 4px 20px rgba(184,150,90,.4) }
.btn-gold:disabled { opacity:.5; cursor:not-allowed }
.btn-outline { background:transparent; border:1px solid var(--canvas-3); color:var(--ink-soft) }
.btn-outline:hover { border-color:var(--gold-light); color:var(--gold-dark) }
.btn-full { width:100% }
.btn svg { width:16px; height:16px; stroke:currentColor; flex-shrink:0 }
.upload-actions { display:flex; gap:.75rem; margin-top:1.5rem }
.result-card { display:none; background:var(--white); border:1px solid rgba(46,125,82,.2); border-radius:var(--radius-lg); padding:2rem; margin-top:1.5rem; animation:fadeUp .4s ease; position:relative; overflow:hidden }
.result-card::before { content:''; position:absolute; top:0; left:0; right:0; height:2px; background:linear-gradient(90deg, var(--success), rgba(46,125,82,.3)) }
.result-card.show { display:block }
.result-header { display:flex; align-items:center; gap:.75rem; margin-bottom:1.25rem }
.result-check { width:32px; height:32px; background:rgba(46,125,82,.1); border-radius:50%; display:flex; align-items:center; justify-content:center; flex-shrink:0 }
.result-check svg { width:16px; height:16px; stroke:var(--success) }
.result-title { font-family:var(--font-display); font-size:1.1rem; font-weight:400; color:var(--ink) }
.result-expires { font-size:.75rem; color:var(--ink-muted) }
.link-row { display:flex; align-items:center; gap:.5rem; margin-bottom:.75rem }
.link-label { font-size:.7rem; font-weight:500; letter-spacing:.1em; text-transform:uppercase; color:var(--ink-muted); min-width:4.5rem }
.link-field { flex:1; display:flex; align-items:center; background:var(--canvas); border:1px solid var(--canvas-3); border-radius:var(--radius-sm); overflow:hidden }
.link-input { flex:1; padding:.55rem .75rem; font-size:.82rem; font-family:'Courier New', monospace; color:var(--ink-soft); background:transparent; border:none; outline:none; white-space:nowrap; overflow:hidden; text-overflow:ellipsis }
.copy-btn { padding:.55rem .75rem; border:none; border-left:1px solid var(--canvas-3); background:transparent; cursor:pointer; color:var(--ink-muted); font-size:.75rem; font-weight:500; letter-spacing:.05em; transition:all .2s; white-space:nowrap }
.copy-btn:hover { background:var(--canvas-2); color:var(--gold-dark) }
.copy-btn.copied { color:var(--success) }
.alert { display:none; padding:.875rem 1.25rem; border-radius:var(--radius-sm); font-size:.85rem; margin-top:1rem; border:1px solid }
.alert.show { display:block }
.alert-error { background:rgba(192,57,43,.06); border-color:rgba(192,57,43,.2); color:var(--danger) }
.alert-info { background:rgba(184,150,90,.06); border-color:rgba(184,150,90,.2); color:var(--gold-dark) }
.view-wrap { max-width:900px; margin:0 auto; animation:fadeUp .6s ease }
.view-header { margin-bottom:2rem }
.view-meta { display:flex; flex-wrap:wrap; gap:.5rem 1.5rem; font-size:.8rem; color:var(--ink-muted); margin-bottom:.75rem }
.view-meta span { display:flex; align-items:center; gap:.35rem }
.view-meta svg { width:13px; height:13px; stroke:var(--gold) }
.view-title { font-family:var(--font-display); font-size:clamp(1.5rem, 3vw, 2.2rem); font-weight:300; letter-spacing:-.01em; color:var(--ink); word-break:break-all }
.view-image-wrap { background:var(--white); border:1px solid var(--canvas-3); border-radius:var(--radius-lg); overflow:hidden; box-shadow:var(--shadow-xl); margin-bottom:2rem }
.view-image-wrap img { width:100%; display:block; max-height:70vh; object-fit:contain; background:repeating-conic-gradient(var(--canvas-2) 0% 25%, var(--white) 0% 50%) 0 0 / 20px 20px }
.view-actions { display:flex; flex-wrap:wrap; gap:.75rem; margin-bottom:2rem }
.view-expiry { background:var(--white); border:1px solid var(--canvas-3); border-radius:var(--radius); padding:1rem 1.25rem; display:flex; align-items:center; gap:.75rem; font-size:.85rem }
.view-expiry svg { width:16px; height:16px; stroke:var(--gold); flex-shrink:0 }
.view-expiry strong { color:var(--gold-dark) }
.not-found { text-align:center; padding:4rem 2rem; max-width:480px; margin:0 auto }
.not-found-icon { font-size:3.5rem; margin-bottom:1.5rem; filter:grayscale(.4) }
.not-found h2 { font-family:var(--font-display); font-size:2rem; font-weight:300; margin-bottom:.75rem }
.not-found p { color:var(--ink-muted); font-weight:300; margin-bottom:2rem }
.features { display:grid; grid-template-columns:repeat(auto-fit, minmax(200px, 1fr)); gap:1.25rem; margin-top:4rem; max-width:600px; margin-left:auto; margin-right:auto; animation:fadeUp .8s ease .3s both }
.feature { background:var(--white); border:1px solid var(--canvas-3); border-radius:var(--radius); padding:1.25rem 1.5rem; display:flex; align-items:flex-start; gap:.875rem }
.feature-icon { width:36px; height:36px; background:rgba(184,150,90,.08); border-radius:var(--radius-sm); display:flex; align-items:center; justify-content:center; flex-shrink:0 }
.feature-icon svg { width:16px; height:16px; stroke:var(--gold) }
.feature-text h4 { font-size:.82rem; font-weight:500; color:var(--ink); margin-bottom:.2rem }
.feature-text p { font-size:.76rem; color:var(--ink-muted); font-weight:300; line-height:1.5 }
footer { border-top:1px solid var(--canvas-3); padding:1.75rem max(2rem, calc(50% - 38rem)); display:flex; align-items:center; justify-content:space-between; flex-wrap:wrap; gap:.75rem; background:rgba(247,245,242,.95) }
.footer-left { font-size:.78rem; color:var(--ink-muted) }
.footer-left a { color:var(--gold-dark); text-decoration:none; font-weight:500 }
.footer-left a:hover { text-decoration:underline }
.footer-right { display:flex; align-items:center; gap:.5rem; font-size:.78rem; color:var(--ink-muted) }
.footer-dc { display:flex; align-items:center; gap:.4rem; text-decoration:none; color:var(--ink-soft); font-weight:500; transition:color .2s }
.footer-dc:hover { color:var(--gold-dark) }
.footer-dc-logo { width:20px; height:20px; background:linear-gradient(135deg, var(--gold-dark), var(--gold)); border-radius:4px; display:flex; align-items:center; justify-content:center; font-size:9px; color:white; font-weight:700; letter-spacing:-.02em }
@keyframes fadeUp { opacity:1; transform:translateY(0); } }
@media (max-width: 640px) { flex-direction:column; text-align:center; } }
</style>
</head>
<body>
<header class="header">
<a href="/" class="logo">
<span class="logo-mark">PixelDrop</span>
<span class="logo-dot"></span>
<span class="logo-sub">Bildhosting</span>
</a>
<span class="header-badge">Kostenlos · Sicher · 7 Tage</span>
</header>
<main>
<?php if ($view_token && $view_data === null): ?>
<!-- NOT FOUND -->
<div class="not-found">
<div class="not-found-icon">🌫</div>
<h2>Bild nicht gefunden</h2>
<p>Dieses Bild existiert nicht mehr oder ist abgelaufen. Bilder werden nach <?= EXPIRY_DAYS ?> Tagen automatisch gelöscht.</p>
<a href="/" class="btn btn-gold">Neues Bild hochladen</a>
</div>
<?php elseif ($view_data): ?>
<!-- VIEW PAGE -->
<div class="view-wrap">
<div class="view-header">
<div class="view-meta">
<span><svg fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/></svg> <?= date('d.m.Y', $view_data['uploaded_at']) ?></span>
<span><svg fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/></svg> <?= formatBytes($view_data['file_size']) ?></span>
<?php if ($view_data['width']): ?>
<span><svg fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 5l-5-5m5 5v-4m0 4h-4"/></svg> <?= $view_data['width'] ?>×<?= $view_data['height'] ?></span>
<?php endif; ?>
<span><svg fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/></svg> <?= number_format($view_data['views']) ?> Aufrufe</span>
</div>
<h1 class="view-title"><?= htmlspecialchars($view_data['original_name']) ?></h1>
</div>
<div class="view-image-wrap">
<img src="<?= SITE_URL . '/uploads/' . htmlspecialchars($view_data['filename']) ?>" alt="<?= htmlspecialchars($view_data['original_name']) ?>" loading="lazy">
</div>
<div class="view-actions">
<a href="<?= SITE_URL . '/uploads/' . htmlspecialchars($view_data['filename']) ?>" download="<?= htmlspecialchars($view_data['original_name']) ?>" class="btn btn-gold">
<svg fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"/></svg>
Herunterladen
</a>
<button class="btn btn-outline" onclick="copyToClipboard('<?= SITE_URL . '/' . $view_data['token'] ?>', this)">
<svg fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"/></svg>
Link kopieren
</button>
<a href="/" class="btn btn-outline">
<svg fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M12 4v16m8-8H4"/></svg>
Neues Bild
</a>
</div>
<div class="view-expiry">
<svg fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
<span>Dieses Bild wird automatisch in <strong><?= timeLeft($view_data['expires_at']) ?></strong> gelöscht (<?= date('d.m.Y', $view_data['expires_at']) ?>).</span>
</div>
</div>
<?php else: ?>
<!-- UPLOAD PAGE -->
<div class="hero">
<div class="hero-eyebrow">Schnell & Sicher</div>
<h1>Teile deine <em>Bilder</em><br>mit der Welt</h1>
<p>Lade ein Bild hoch und erhalte sofort einen Link. Keine Registrierung. Automatisch nach <?= EXPIRY_DAYS ?> Tagen gelöscht.</p>
</div>
<div class="upload-card">
<form id="uploadForm">
<input type="hidden" name="csrf" value="<?= htmlspecialchars($_SESSION['csrf']) ?>">
<div class="dropzone" id="dropzone">
<input type="file" name="image" id="fileInput" accept="image/jpeg,image/png,image/gif,image/webp,image/svg+xml">
<div class="drop-icon">
<svg fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"/></svg>
</div>
<p class="drop-title">Bild hierher ziehen</p>
<p class="drop-sub">oder <strong>klicken zum Auswählen</strong><br>JPG, PNG, GIF, WebP, SVG — max. <strong>10 MB</strong></p>
</div>
<div class="preview-wrap" id="previewWrap">
<img class="preview-img" id="previewImg" src="" alt="Vorschau">
<button type="button" class="preview-remove" id="removeBtn">
<svg fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"/></svg>
</button>
<div class="preview-info" id="previewInfo"></div>
</div>
<div class="progress-wrap" id="progressWrap">
<div class="progress-bar-track">
<div class="progress-bar-fill" id="progressFill"></div>
</div>
<p class="progress-label" id="progressLabel">Wird hochgeladen…</p>
</div>
<div class="alert alert-error" id="errorAlert"></div>
<div class="upload-actions">
<button type="submit" class="btn btn-gold btn-full" id="uploadBtn" disabled>
<svg fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12"/></svg>
Bild hochladen
</button>
</div>
</form>
<div class="result-card" id="resultCard">
<div class="result-header">
<div class="result-check">
<svg fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></svg>
</div>
<div>
<p class="result-title">Bild erfolgreich hochgeladen</p>
<p class="result-expires" id="resultExpiry"></p>
</div>
</div>
<div class="link-row">
<span class="link-label">Share</span>
<div class="link-field">
<input class="link-input" id="shareUrl" type="text" readonly>
<button class="copy-btn" onclick="copyField('shareUrl', this)">Kopieren</button>
</div>
</div>
<div class="link-row">
<span class="link-label">Direkt</span>
<div class="link-field">
<input class="link-input" id="directUrl" type="text" readonly>
<button class="copy-btn" onclick="copyField('directUrl', this)">Kopieren</button>
</div>
</div>
<div style="margin-top:1.25rem; display:flex; gap:.75rem; flex-wrap:wrap;">
<a id="viewLink" href="#" target="_blank" class="btn btn-gold" style="flex:1;">
<svg fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/></svg>
Bild anzeigen
</a>
<button class="btn btn-outline" onclick="resetUpload()" style="flex:1;">Neues Bild</button>
</div>
</div>
</div>
<div class="features">
<div class="feature"><div class="feature-icon"><svg fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M13 10V3L4 14h7v7l9-11h-7z"/></svg></div><div class="feature-text"><h4>Blitzschnell</h4><p>Upload in Sekunden, Link sofort einsatzbereit</p></div></div>
<div class="feature"><div class="feature-icon"><svg fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"/></svg></div><div class="feature-text"><h4>Sicher</h4><p>Keine Registrierung, keine Datensammlung</p></div></div>
<div class="feature"><div class="feature-icon"><svg fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/></svg></div><div class="feature-text"><h4>Auto-Delete</h4><p>Bilder werden nach 7 Tagen automatisch gelöscht</p></div></div>
</div>
<?php endif; ?>
</main>
<footer>
<p class="footer-left">
© <?= date('Y') ?> PixelDrop — Bildhosting von
<a href="https://www.dreamcodes.net" target="_blank" rel="noopener">Dreamcodes.net</a>
</p>
<div class="footer-right">
<span>Powered by</span>
<a href="https://www.dreamcodes.net" target="_blank" rel="noopener" class="footer-dc">
<span class="footer-dc-logo">DC</span>
Dreamcodes
</a>
</div>
</footer>
<script>
const gi=id=>document.getElementById(id);
const dropzone=gi('dropzone'),fileInput=gi('fileInput'),previewWrap=gi('previewWrap'),previewImg=gi('previewImg'),previewInfo=gi('previewInfo');
const removeBtn=gi('removeBtn'),uploadBtn=gi('uploadBtn'),progressWrap=gi('progressWrap'),progressFill=gi('progressFill'),progressLabel=gi('progressLabel');
const errorAlert=gi('errorAlert'),resultCard=gi('resultCard'),uploadForm=gi('uploadForm');
if (uploadForm) {
dropzone.addEventListener('dragover', e => { e.preventDefault(); dropzone.classList.add('dragover'); });
dropzone.addEventListener('dragleave', () => dropzone.classList.remove('dragover'));
dropzone.addEventListener('drop',e=>{e.preventDefault();dropzone.classList.remove('dragover');const f=e.dataTransfer.files[0];if(f)setFile(f);});
fileInput.addEventListener('change',()=>{if(fileInput.files[0])setFile(fileInput.files[0]);});
removeBtn.addEventListener('click',resetUpload);
uploadForm.addEventListener('submit',async e=>{e.preventDefault();if(!fileInput.files[0])return;await doUpload(fileInput.files[0]);});
}
function setFile(file){
const allowed=['image/jpeg','image/png','image/gif','image/webp','image/svg+xml'];
if(!allowed.includes(file.type)){showError('Ungültiger Dateityp. Bitte JPG, PNG, GIF, WebP oder SVG verwenden.');return;}
if(file.size>10*1024*1024){showError('Datei zu groß. Maximum 10 MB.');return;}
hideError();
const dt=new DataTransfer();dt.items.add(file);fileInput.files=dt.files;
const r=new FileReader();r.onload=ev=>{previewImg.src=ev.target.result;dropzone.style.display='none';previewWrap.classList.add('show');previewInfo.innerHTML=`<span>${file.name}</span><span>${formatBytes(file.size)}</span><span>${file.type.split('/')[1].toUpperCase()}</span>`;uploadBtn.disabled=false;};r.readAsDataURL(file);
}
async function doUpload(file){
uploadBtn.disabled=true;uploadBtn.innerHTML='<span style="opacity:.6">Wird hochgeladen…</span>';progressWrap.classList.add('show');hideError();
const formData=new FormData(uploadForm);
return new Promise((resolve)=>{
const xhr = new XMLHttpRequest();
xhr.open('POST', '?action=upload');
xhr.upload.onprogress=e=>{if(e.lengthComputable){const p=Math.round(e.loaded/e.total*100);progressFill.style.width=p+'%';progressLabel.textContent=p<100?`${p}% hochgeladen…`:'Verarbeitung…';}};
xhr.onload=()=>{progressWrap.classList.remove('show');let d;try{d=JSON.parse(xhr.responseText);}catch(e){showError('Unerwarteter Serverfehler');resetBtn();resolve();return;}d.error?(showError(d.error),resetBtn()):showResult(d);resolve();};
xhr.onerror=()=>{progressWrap.classList.remove('show');showError('Netzwerkfehler. Bitte erneut versuchen.');resetBtn();resolve();};
xhr.send(formData);
});
}
function showResult(d){gi('shareUrl').value=d.url;gi('directUrl').value=d.direct;gi('viewLink').href=d.url;gi('resultExpiry').textContent=`Läuft ab am: ${d.expires}`+(d.dimensions?` · ${d.dimensions}`:'')+` · ${d.size}`;resultCard.classList.add('show');}
function resetBtn(){uploadBtn.disabled=false;uploadBtn.innerHTML='<svg fill="none" viewBox="0 0 24 24" style="width:16px;height:16px;stroke:currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12"/></svg> Bild hochladen';}
function resetUpload(){fileInput.value='';previewImg.src='';previewWrap.classList.remove('show');dropzone.style.display='';uploadBtn.disabled=true;resultCard.classList.remove('show');progressWrap.classList.remove('show');progressFill.style.width='0';hideError();resetBtn();}
function showError(msg){errorAlert.textContent=msg;errorAlert.classList.add('show');}
function hideError(){errorAlert.classList.remove('show');}
function copyField(id,btn){navigator.clipboard.writeText(gi(id).value).then(()=>{const o=btn.textContent;btn.textContent='✓ Kopiert';btn.classList.add('copied');setTimeout(()=>{btn.textContent=o;btn.classList.remove('copied');},2000);});}
function copyToClipboard(t,btn){navigator.clipboard.writeText(t).then(()=>{const o=btn.innerHTML;btn.innerHTML='<svg fill="none" viewBox="0 0 24 24" style="width:16px;height:16px;stroke:currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></svg> Kopiert!';setTimeout(()=>{btn.innerHTML=o;},2000);});}
function formatBytes(b){return b>=1048576?(b/1048576).toFixed(1)+' MB':b>=1024?(b/1024).toFixed(1)+' KB':b+' B';}
</script>
</body>
</html>
