false, 'message' => 'Project ID required'], 400); } updateProject($id); break; case 'DELETE': requireAuth(); if (!$id) { jsonResponse(['success' => false, 'message' => 'Project ID required'], 400); } deleteProject($id); break; default: jsonResponse(['success' => false, 'message' => 'Method not allowed'], 405); } function getProjects() { global $db; $projects = $db->getAll('portfolio'); // Apply filters $category = $_GET['category'] ?? null; $featured = $_GET['featured'] ?? null; $limit = (int)($_GET['limit'] ?? 20); $offset = (int)($_GET['offset'] ?? 0); if ($category) { $projects = array_filter($projects, function($p) use ($category) { return $p['category'] === $category; }); } if ($featured !== null) { $projects = array_filter($projects, function($p) use ($featured) { return ($p['featured'] ?? false) == ($featured === 'true' || $featured === '1'); }); } // Public view: only published/active projects if (!checkAuth()) { $projects = array_filter($projects, function($p) { return ($p['status'] ?? 'published') === 'published'; }); } // Sort by order or date usort($projects, function($a, $b) { $orderA = $a['order'] ?? 999; $orderB = $b['order'] ?? 999; if ($orderA !== $orderB) { return $orderA - $orderB; } return strtotime($b['created_at']) - strtotime($a['created_at']); }); $total = count($projects); $projects = array_slice(array_values($projects), $offset, $limit); jsonResponse([ 'success' => true, 'data' => $projects, 'total' => $total, 'limit' => $limit, 'offset' => $offset ]); } function getProject($id) { global $db; $project = $db->get('portfolio', $id); if (!$project) { jsonResponse(['success' => false, 'message' => 'Project not found'], 404); } jsonResponse([ 'success' => true, 'data' => $project ]); } function createProject() { global $db; // Handle multipart form data or JSON if (!empty($_FILES)) { $data = $_POST; } else { $data = getRequestBody(); } // Validate required fields if (empty($data['title'])) { jsonResponse(['success' => false, 'message' => 'Title is required'], 400); } // Sanitize text fields (allow HTML in description) $data['title'] = sanitize($data['title']); $data['category'] = sanitize($data['category'] ?? ''); $data['client'] = sanitize($data['client'] ?? ''); $data['technologies'] = sanitize($data['technologies'] ?? ''); // Sanitize description: allow only safe formatting tags $data['description'] = sanitizeRichText($data['description'] ?? ''); // Handle image upload if (!empty($_FILES['image'])) { $upload = handleFileUpload($_FILES['image'], 'portfolio'); if ($upload['success']) { $data['image'] = $upload['path']; } } // Parse results JSON (JS sends as a serialised string) $data['results'] = json_decode($data['results'] ?? '[]', true) ?? []; // Gallery: merge existing-keep list + newly uploaded files $gallery = json_decode($_POST['gallery_keep'] ?? '[]', true) ?? []; if (!empty($_FILES['gallery_images']['tmp_name'])) { foreach ($_FILES['gallery_images']['tmp_name'] as $key => $tmpName) { if ($_FILES['gallery_images']['error'][$key] !== UPLOAD_ERR_OK) continue; $file = [ 'name' => $_FILES['gallery_images']['name'][$key], 'type' => $_FILES['gallery_images']['type'][$key], 'tmp_name' => $tmpName, 'error' => $_FILES['gallery_images']['error'][$key], 'size' => $_FILES['gallery_images']['size'][$key], ]; $upload = handleFileUpload($file, 'portfolio'); if ($upload['success']) { $gallery[] = $upload['path']; } } } $data['gallery'] = $gallery; // Set defaults $data['featured'] = $data['featured'] ?? false; $data['order'] = (int)($data['order'] ?? 0); $data['status'] = $data['status'] ?? 'published'; $project = $db->insert('portfolio', $data); jsonResponse([ 'success' => true, 'message' => 'Project created successfully', 'data' => $project ], 201); } function updateProject($id) { global $db; $existing = $db->get('portfolio', $id); if (!$existing) { jsonResponse(['success' => false, 'message' => 'Project not found'], 404); } // Handle multipart form data or JSON if (!empty($_FILES)) { $data = $_POST; } else { $data = getRequestBody(); } // Handle image upload if (!empty($_FILES['image'])) { $upload = handleFileUpload($_FILES['image'], 'portfolio'); if ($upload['success']) { $data['image'] = $upload['path']; } } // Sanitize editable text fields if (isset($data['title'])) $data['title'] = sanitize($data['title']); if (isset($data['category'])) $data['category'] = sanitize($data['category']); if (isset($data['client'])) $data['client'] = sanitize($data['client']); if (isset($data['technologies'])) $data['technologies'] = sanitize($data['technologies']); if (isset($data['description'])) $data['description'] = sanitizeRichText($data['description']); // Parse results JSON if (isset($data['results'])) { $data['results'] = json_decode($data['results'], true) ?? $existing['results'] ?? []; } // Gallery: merge existing-keep list + newly uploaded files if (array_key_exists('gallery_keep', $_POST)) { $gallery = json_decode($_POST['gallery_keep'], true) ?? []; if (!empty($_FILES['gallery_images']['tmp_name'])) { foreach ($_FILES['gallery_images']['tmp_name'] as $key => $tmpName) { if ($_FILES['gallery_images']['error'][$key] !== UPLOAD_ERR_OK) continue; $file = [ 'name' => $_FILES['gallery_images']['name'][$key], 'type' => $_FILES['gallery_images']['type'][$key], 'tmp_name' => $tmpName, 'error' => $_FILES['gallery_images']['error'][$key], 'size' => $_FILES['gallery_images']['size'][$key], ]; $upload = handleFileUpload($file, 'portfolio'); if ($upload['success']) { $gallery[] = $upload['path']; } } } $data['gallery'] = $gallery; } $project = $db->update('portfolio', $id, $data); jsonResponse([ 'success' => true, 'message' => 'Project updated successfully', 'data' => $project ]); } function deleteProject($id) { global $db; $existing = $db->get('portfolio', $id); if (!$existing) { jsonResponse(['success' => false, 'message' => 'Project not found'], 404); } $db->delete('portfolio', $id); jsonResponse([ 'success' => true, 'message' => 'Project deleted successfully' ]); }