gagag
thumb thumb

gagag

Product ID: 5 · Added: 2025-11-03 · Views: 0
<?php
// admin/add_product.php (AJAX + progress + UX improvements)
require '../config.php';
if (empty($_SESSION['admin_logged'])) { header('Location: login.php'); exit; }

// ensure upload dir exists
$uploadDir = __DIR__ . '/../uploads/';
if (!is_dir($uploadDir)) mkdir($uploadDir, 0755, true);

// CSRF token
if (empty($_SESSION['csrf_token'])) $_SESSION['csrf_token'] = bin2hex(random_bytes(24));
$csrf = $_SESSION['csrf_token'];

// helpers
if (!function_exists('e')) {
function e($s) { return htmlspecialchars($s ?? '', ENT_QUOTES); }
}
function showVal($v) { return htmlspecialchars($v ?? '', ENT_QUOTES); }
function slugify($text) {
$text = preg_replace('~[^\pL\d]+~u', '-', $text);
$text = iconv('UTF-8', 'ASCII//TRANSLIT');
$text = preg_replace('~[^-\w]+~', '', $text);
$text = trim($text, '-');
$text = preg_replace('~-+~', '-', $text);
$text = strtolower($text);
return $text ?: 'product-'.bin2hex(random_bytes(3));
}

// detect AJAX (fetch/XHR)
$looksLikeAjax = (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest')
|| (strpos($_SERVER['HTTP_ACCEPT'] ?? '', 'application/json') !== false);

$err = '';
$msg = '';
$lastImage = null;

// server-side handler (same logic as before, but returns JSON if AJAX)
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// CSRF check (works for AJAX and normal posts)
$posted_csrf = $_POST['csrf_token'] ?? '';
if (empty($_SESSION['csrf_token']) || !hash_equals($_SESSION['csrf_token'], $posted_csrf)) {
$err = "Invalid request (CSRF). Please refresh and try again.";
if ($looksLikeAjax) { echo json_encode(['success'=>false,'message'=>$err]); exit; }
} else {
// Receive & sanitize server-side
$title = trim($_POST['title'] ?? '');
$slug = trim($_POST['slug'] ?? '');
$description = trim($_POST['description'] ?? '');
$price = (float)($_POST['price'] ?? 0);
$tags = trim($_POST['tags'] ?? '');
$featured = isset($_POST['featured']) ? 1 : 0;

// Server-side tag normalization: limit to 6 tags, each <= 40 chars
$tagsArr = [];
if ($tags !== '') {
$raw = array_filter(array_map('trim', explode(',', $tags)));
foreach ($raw as $t) {
$t2 = mb_strtolower(preg_replace('/[^a-z0-9\- ]+/i','', $t));
if ($t2 !== '') $tagsArr[] = mb_substr($t2,0,40);
if (count($tagsArr) >= 6) break;
}
$tags = implode(',', $tagsArr);
}

// Basic validations
if ($title === '') $err = "Product title is required.";
elseif ($price <= 0) $err = "Price must be greater than 0.";
else {
// Duplicate-title check (best-effort)
try {
$stDup = $pdo->prepare("SELECT id FROM products WHERE title = ? LIMIT 1");
$stDup->execute([$title]);
if ($stDup->fetch()) $err = "A product with this title already exists.";
} catch (Throwable $t) {
// ignore DB issues here, allow insert attempt
}
}

// prepare slug server-side if empty
if (!$err) {
if ($slug === '') {
// simple slug: lower + hyphens
$slug = preg_replace('/[^\w\s-]/', '', $title);
$slug = preg_replace('/\s+/', '-', trim($slug));
$slug = strtolower($slug);
if ($slug === '') $slug = 'product-'.bin2hex(random_bytes(4));
} else {
$slug = preg_replace('/[^\w\s-]/', '', $slug);
$slug = preg_replace('/\s+/', '-', trim($slug));
$slug = strtolower($slug);
}
}

// handle image upload
if (!$err && !empty($_FILES['image']['name'])) {
$file = $_FILES['image'];
$maxSize = 3 * 1024 * 1024; // 3MB
$mimeExtMap = [
'image/jpeg' => 'jpg',
'image/png' => 'png',
'image/webp' => 'webp'
];

if ($file['error'] !== UPLOAD_ERR_OK) {
$err = "Image upload error (code {$file['error']}).";
} elseif ($file['size'] > $maxSize) {
$err = "Image too large. Max 3MB allowed.";
} else {
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mime = finfo_file($finfo, $file['tmp_name']);
finfo_close($finfo);

if (!isset($mimeExtMap[$mime])) {
$err = "Unsupported image type. Allowed: JPG, PNG, WEBP.";
} else {
$ext = $mimeExtMap[$mime];
$safeName = bin2hex(random_bytes(10)) . '.' . $ext;
$target = $uploadDir . $safeName;

if (!move_uploaded_file($file['tmp_name'], $target)) {
$err = "Failed to move uploaded file.";
} else {
// try to resize and create thumbnail (best-effort)
try {
if (function_exists('getimagesize') && function_exists('imagecreatetruecolor')) {
list($w,$h) = getimagesize($target);
// resize original down to max width 1400
$maxW = 1400;
if ($w > $maxW) {
$ratio = $maxW / $w;
$newW = (int)($w * $ratio);
$newH = (int)($h * $ratio);
} else { $newW = $w; $newH = $h; }

if ($ext === 'jpg') $src = imagecreatefromjpeg($target);
elseif ($ext === 'png') $src = imagecreatefrompng($target);
elseif ($ext === 'webp' && function_exists('imagecreatefromwebp')) $src = imagecreatefromwebp($target);
else $src = false;

if ($src !== false) {
$dst = imagecreatetruecolor($newW, $newH);
if ($ext === 'png') { imagealphablending($dst, false); imagesavealpha($dst, true); }
imagecopyresampled($dst,$src,0,0,0,0,$newW,$newH,$w,$h);
if ($ext === 'jpg') imagejpeg($dst, $target, 85);
elseif ($ext === 'png') imagepng($dst, $target, 6);
elseif ($ext === 'webp' && function_exists('imagewebp')) imagewebp($dst, $target, 85);
imagedestroy($dst); imagedestroy($src);
}

// thumbnail
$thumbW = 600; $thumbH = (int)(($thumbW / max(1,$w)) * $h);
$thumb = imagecreatetruecolor($thumbW,$thumbH);
if ($ext === 'jpg') $src2 = imagecreatefromjpeg($target);
elseif ($ext === 'png') $src2 = imagecreatefrompng($target);
elseif ($ext === 'webp' && function_exists('imagecreatefromwebp')) $src2 = imagecreatefromwebp($target);
else $src2 = false;

if ($src2 !== false) {
if ($ext === 'png') { imagealphablending($thumb,false); imagesavealpha($thumb,true); }
imagecopyresampled($thumb, $src2,0,0,0,0,$thumbW,$thumbH,$w,$h);
$thumbPath = $uploadDir . 'thumb_' . $safeName;
if ($ext === 'jpg') imagejpeg($thumb, $thumbPath, 82);
elseif ($ext === 'png') imagepng($thumb, $thumbPath, 6);
elseif ($ext === 'webp' && function_exists('imagewebp')) imagewebp($thumb, $thumbPath, 82);
imagedestroy($thumb); imagedestroy($src2);
}
}
} catch (Throwable $t) {
// ignore image processing errors
}

$lastImage = $safeName;
}
}
}
}

// insert into DB (attempt to include slug/tags/featured if columns exist)
if (!$err) {
try {
$cols = ['title','description','price','image'];
$placeholders = ['?','?','?','?'];
$values = [$title,$description,$price,$lastImage];

// detect slug/tags/featured columns (best-effort)
try {
$hasSlug = (bool)$pdo->query("SHOW COLUMNS FROM products LIKE 'slug'")->fetch();
$hasTags = (bool)$pdo->query("SHOW COLUMNS FROM products LIKE 'tags'")->fetch();
$hasFeatured = (bool)$pdo->query("SHOW COLUMNS FROM products LIKE 'featured'")->fetch();
} catch (Throwable $t) { $hasSlug = $hasTags = $hasFeatured = false; }

if ($hasSlug) { $cols[] = 'slug'; $placeholders[]='?'; $values[] = $slug; }
if ($hasTags) { $cols[] = 'tags'; $placeholders[]='?'; $values[] = $tags; }
if ($hasFeatured) { $cols[] = 'featured'; $placeholders[]='?'; $values[] = $featured; }

$sql = "INSERT INTO products (" . implode(',', $cols) . ") VALUES (" . implode(',', $placeholders) . ")";
$st = $pdo->prepare($sql);
$st->execute($values);

// success
$msg = "Product added successfully.";
// rotate CSRF
$_SESSION['csrf_token'] = bin2hex(random_bytes(24));
$csrf = $_SESSION['csrf_token'];

if ($looksLikeAjax) {
// return JSON with info including preview url
$thumbUrl = $lastImage ? '../uploads/thumb_' . $lastImage : ($lastImage ? '../uploads/' . $lastImage : '');
echo json_encode(['success'=>true,'message'=>$msg,'image'=>$lastImage ? ('../uploads/'.$lastImage) : null,'thumb'=>$thumbUrl]);
exit;
} else {
// non-AJAX: reload page to clear form
header('Location: add_product.php?ok=1');
exit;
}
} catch (Throwable $t) {
$err = "Database error: " . $t->getMessage();
// cleanup uploaded files to avoid orphan
if ($lastImage) {
@unlink($uploadDir . $lastImage);
@unlink($uploadDir . 'thumb_' . $lastImage);
$lastImage = null;
}
if ($looksLikeAjax) { echo json_encode(['success'=>false,'message'=>$err]); exit; }
}
} else {
if ($looksLikeAjax) { echo json_encode(['success'=>false,'message'=>$err]); exit; }
}
}
}

// fetch recent products for list
$products = $pdo->query("SELECT id,title,price,image,views,created_at FROM products ORDER BY created_at DESC LIMIT 50")->fetchAll(PDO::FETCH_ASSOC);
?>
<!doctype html>
<html>
<head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>Add Product — Admin</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
.thumb { max-width:140px; max-height:90px; object-fit:cover; border-radius:6px; }
.small-muted { font-size:.9rem; color:#6c757d; }
.drop-area { border: 2px dashed #e6eefc; padding:14px; border-radius:10px; cursor:pointer; background:#fff; }
.drop-area.dragover { border-color:#0d6efd; box-shadow:0 8px 24px rgba(13,110,253,0.06); }
.progress { height:10px; border-radius:6px; overflow:hidden; background:#eef3ff; }
.progress-bar { background:linear-gradient(90deg,#0d6efd,#1558ff); width:0%; height:100%; transition:width .15s; }
.alert-placeholder { min-height:58px; }
</style>
</head>
<body>
<div class="container my-4">
<div class="d-flex justify-content-between align-items-center mb-3">
<h3>Add Product</h3>
<div>
<a class="btn btn-outline-secondary" href="dashboard.php">&larr; Dashboard</a>
<a class="btn btn-outline-primary" href="products.php">Manage products</a>
</div>
</div>

<!-- inline alert area -->
<div id="alertArea" class="alert-placeholder">
<?php if (!empty($_GET['ok'])): ?>
<div class="alert alert-success alert-dismissible fade show"><?php echo e("Product added successfully."); ?><button class="btn-close" data-bs-dismiss="alert"></button></div>
<?php endif; ?>
</div>

<div class="row g-4">
<div class="col-lg-7">
<div class="card shadow-sm"><div class="card-body">
<form id="productForm" method="post" enctype="multipart/form-data" novalidate>
<input type="hidden" name="csrf_token" value="<?php echo e($csrf); ?>">

<div class="mb-3 row">
<div class="col-md-8">
<label class="form-label">Title <span class="text-danger">*</span></label>
<input type="text" id="title" name="title" class="form-control" required value="<?php echo showVal($title ?? ''); ?>" aria-describedby="titleHelp">
<div id="titleHelp" class="form-text small-muted">Use a descriptive title.</div>
</div>
<div class="col-md-4">
<label class="form-label">Price (INR) <span class="text-danger">*</span></label>
<input type="number" step="0.01" min="0" id="price" name="price" class="form-control" required value="<?php echo showVal($price ?? ''); ?>">
</div>
</div>

<div class="mb-3">
<label class="form-label">Slug (URL friendly)</label>
<input type="text" id="slug" name="slug" class="form-control" value="<?php echo showVal($slug ?? ''); ?>" placeholder="auto-generated from title">
<div class="form-text small-muted">Lowercase, hyphens only. Leave empty to auto-generate.</div>
</div>

<div class="mb-3">
<label class="form-label">Description</label>
<textarea name="description" id="description" class="form-control" rows="6"><?php echo showVal($description ?? ''); ?></textarea>
</div>

<div class="mb-3">
<label class="form-label">Tags (comma separated)</label>
<input type="text" id="tags" name="tags" class="form-control" placeholder="e.g. templates, productivity" value="<?php echo showVal($tags ?? ''); ?>">
<div class="form-text small-muted">Optional. Max 6 tags; client normalizes them.</div>
</div>

<div class="row g-3 align-items-center mb-3">
<div class="col-md-8">
<label class="form-label">Product image (JPG/PNG/WEBP) — max 3MB</label>
<div id="dropArea" class="drop-area" tabindex="0" role="button" aria-label="Upload image">
<div class="d-flex align-items-center gap-3">
<div style="width:84px;height:58px; display:flex;align-items:center;justify-content:center;">
<img id="previewImg" src="<?php echo $lastImage ? '../uploads/'.e($lastImage) : ''; ?>" alt="" style="max-width:84px; max-height:58px; object-fit:cover; display:<?php echo $lastImage ? 'block' : 'none'; ?>; border-radius:6px;">
</div>
<div>
<div class="fw-medium">Click to choose or drag & drop an image</div>
<div class="small-muted">Square or wide images work best.</div>
</div>
</div>
<input id="imageInput" type="file" name="image" accept="image/*" style="display:none;">
</div>
</div>

<div class="col-md-4">
<label class="form-label">Flags</label>
<div class="form-check">
<input class="form-check-input" type="checkbox" name="featured" id="featured" <?php echo !empty($featured) ? 'checked' : ''; ?>>
<label class="form-check-label" for="featured">Featured (shows ribbon)</label>
</div>

<div class="mt-3">
<div class="small-muted mb-1">Upload progress</div>
<div class="progress" aria-hidden="true"><div id="uploadBar" class="progress-bar" style="width:0%"></div></div>
<div id="uploadPct" class="small-muted mt-1">0%</div>
</div>
</div>
</div>

<div class="d-flex gap-2">
<button id="submitBtn" class="btn btn-primary">Add Product</button>
<button id="resetBtn" type="reset" class="btn btn-outline-secondary">Reset</button>
</div>
</form>
</div></div>

<div class="mt-3 small-muted">
PHP server does image validation & creates a thumbnail. If you want instant delivery after payment, I can wire email attachments next.
</div>
</div>

<div class="col-lg-5">
<div class="card shadow-sm"><div class="card-body">
<h5 class="mb-3">Recently added products</h5>
<?php if (empty($products)): ?>
<div class="text-muted small">No products yet. Add one on the left.</div>
<?php else: ?>
<div class="list-group">
<?php foreach ($products as $p): ?>
<div class="list-group-item d-flex gap-3 align-items-center">
<div>
<?php if ($p['image'] && file_exists(__DIR__ . '/../uploads/' . $p['image'])): ?>
<img src="../uploads/<?php echo e($p['image']); ?>" class="thumb" alt="">
<?php else: ?>
<div class="thumb d-flex align-items-center justify-content-center bg-light small-muted">No image</div>
<?php endif; ?>
</div>
<div class="flex-grow-1">
<div class="fw-600"><?php echo e($p['title']); ?></div>
<div class="small-muted">₹ <?php echo number_format($p['price'],2); ?> • Views: <?php echo (int)$p['views']; ?></div>
<div class="small-muted">Added: <?php echo e(substr($p['created_at'],0,10)); ?></div>
</div>
<div class="text-end">
<a class="btn btn-sm btn-outline-primary" href="edit_product.php?id=<?php echo (int)$p['id']; ?>">Edit</a>
<a class="btn btn-sm btn-outline-danger" href="products.php?del=<?php echo (int)$p['id']; ?>" onclick="return confirm('Delete product?')">Delete</a>
<a class="btn btn-sm btn-outline-secondary" href="../product.php?id=<?php echo (int)$p['id']; ?>" target="_blank">View</a>
</div>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div></div>
</div>
</div>

<div class="mt-4 small text-muted">
Tip: Use clear titles and short descriptions. If you want the image to be instantly downloadable after payment, ask and I will wire automatic delivery via email.
</div>
</div>

<script>
// Elements
const dropArea = document.getElementById('dropArea');
const fileInput = document.getElementById('imageInput');
const previewImg = document.getElementById('previewImg');
const submitBtn = document.getElementById('submitBtn');
const productForm = document.getElementById('productForm');
const uploadBar = document.getElementById('uploadBar');
const uploadPct = document.getElementById('uploadPct');
const alertArea = document.getElementById('alertArea');
const titleInput = document.getElementById('title');
const slugInput = document.getElementById('slug');
const tagsInput = document.getElementById('tags');
const MAX_TAGS = 6;

// UX: click to open file dialog
dropArea.addEventListener('click', () => fileInput.click());
fileInput.addEventListener('change', handleFiles);

// drag & drop
['dragenter','dragover'].forEach(ev => {
dropArea.addEventListener(ev, (e) => { e.preventDefault(); e.stopPropagation(); dropArea.classList.add('dragover'); });
});
['dragleave','drop'].forEach(ev => {
dropArea.addEventListener(ev, (e) => { e.preventDefault(); e.stopPropagation(); dropArea.classList.remove('dragover'); });
});
dropArea.addEventListener('drop', (e) => {
const dt = e.dataTransfer; if (!dt) return;
const files = dt.files;
if (files.length) { fileInput.files = files; handleFiles(); }
});

function handleFiles() {
const f = fileInput.files[0];
if (!f) { previewImg.style.display = 'none'; return; }
const allowed = ['image/jpeg','image/png','image/webp'];
if (!allowed.includes(f.type)) {
showAlert('Unsupported image type. Use JPG, PNG or WEBP.', 'danger');
fileInput.value = '';
previewImg.style.display = 'none';
return;
}
if (f.size > 3 * 1024 * 1024) {
showAlert('Image too large — maximum 3MB.', 'danger');
fileInput.value = '';
previewImg.style.display = 'none';
return;
}
const reader = new FileReader();
reader.onload = (ev) => { previewImg.src = ev.target.result; previewImg.style.display = 'block'; };
reader.readAsDataURL(f);
}

// auto-generate slug from title unless user edited slug
let slugTouched = false;
slugInput.addEventListener('input', ()=> slugTouched = true);
titleInput.addEventListener('input', () => {
if (slugTouched) return;
const s = titleInput.value.trim().toLowerCase()
.replace(/[^\w\s-]/g,'').replace(/\s+/g,'-').replace(/-+/g,'-').replace(/^-|-$/g,'');
slugInput.value = s;
});

// client-side tags normalization and limit check
function normalizeTagsRaw(raw) {
if (!raw) return '';
const arr = raw.split(',').map(x => x.trim()).filter(x => x.length);
const norm = [];
for (let t of arr) {
t = t.toLowerCase().replace(/[^a-z0-9\- ]/g,'').substring(0,40);
if (t && !norm.includes(t)) norm.push(t);
if (norm.length >= MAX_TAGS) break;
}
return norm.join(',');
}

// show bootstrap-style alerts in alertArea
function showAlert(msg, type='info', autoHide=true) {
alertArea.innerHTML = `<div class="alert alert-${type} alert-dismissible fade show">${msg}<button type="button" class="btn-close" data-bs-dismiss="alert"></button></div>`;
if (autoHide) setTimeout(()=> {
const node = alertArea.querySelector('.alert');
if (node) node.classList.remove('show');
}, 5000);
}

// submit via XHR to capture progress (falls back to normal submit if JS disabled)
productForm.addEventListener('submit', function(e){
e.preventDefault();

// client validation
const title = titleInput.value.trim();
const price = parseFloat(document.getElementById('price').value);
if (!title) { showAlert('Please enter a product title','danger'); titleInput.focus(); return; }
if (isNaN(price) || price <= 0) { showAlert('Please enter a valid price','danger'); document.getElementById('price').focus(); return; }

// normalize tags client-side
tagsInput.value = normalizeTagsRaw(tagsInput.value);

// prepare form data
const fd = new FormData(productForm);

// disable UI
submitBtn.disabled = true;
uploadBar.style.width = '0%'; uploadPct.textContent = '0%';

// use XHR to show progress
const xhr = new XMLHttpRequest();
xhr.open('POST', 'add_product.php', true);
xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');

xhr.upload.addEventListener('progress', (ev) => {
if (ev.lengthComputable) {
const pct = Math.round((ev.loaded / ev.total) * 100);
uploadBar.style.width = pct + '%';
uploadPct.textContent = pct + '%';
}
});

xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
submitBtn.disabled = false;
try {
const j = JSON.parse(xhr.responseText);
if (j.success) {
showAlert(j.message || 'Saved', 'success');
// show preview image if sent back
if (j.image) { previewImg.src = j.image; previewImg.style.display = 'block'; }
// clear form (keep preview)
productForm.reset();
slugTouched = false;
uploadBar.style.width = '100%';
uploadPct.textContent = '100%';
// optionally append new product to the recent list (left as exercise)
} else {
showAlert(j.message || 'Error saving product', 'danger');
}
} catch (err) {
showAlert('Unexpected server response', 'danger');
}
}
};

xhr.onerror = function() {
submitBtn.disabled = false;
showAlert('Network error uploading file', 'danger');
};

xhr.send(fd);
});

// reset behavior
document.getElementById('resetBtn').addEventListener('click', function(){
previewImg.style.display = 'none';
fileInput.value = '';
uploadBar.style.width = '0%';
uploadPct.textContent = '0%';
slugInput.value = '';
slugTouched = false;
});

</script>
</body>
</html>
₹ 3,000.00
Secure payments • Manual delivery
Need help? Contact us