query("SHOW COLUMNS FROM projects")->fetchAll(PDO::FETCH_ASSOC); $colNames = array_map(static fn($c) => (string)($c['Field'] ?? ''), $cols); $alter = []; if (!in_array('tech_json', $colNames, true)) $alter[] = "ADD COLUMN tech_json LONGTEXT NULL"; if (!in_array('links_json', $colNames, true)) $alter[] = "ADD COLUMN links_json LONGTEXT NULL"; if (!in_array('sort_order', $colNames, true)) $alter[] = "ADD COLUMN sort_order INT NOT NULL DEFAULT 0"; if (!in_array('slug', $colNames, true)) $alter[] = "ADD COLUMN slug VARCHAR(120) NULL"; if (!in_array('short_summary', $colNames, true)) $alter[] = "ADD COLUMN short_summary VARCHAR(240) NULL"; // Ensure summary can hold long text (avoid Data too long errors) foreach ($cols as $c) { if (($c['Field'] ?? '') !== 'summary') continue; $type = strtolower((string)($c['Type'] ?? '')); $null = strtoupper((string)($c['Null'] ?? '')) === 'YES' ? 'NULL' : 'NOT NULL'; if (strpos($type, 'text') === false) $alter[] = "MODIFY summary TEXT {$null}"; break; } if ($alter) { pdo()->exec("ALTER TABLE projects " . implode(", ", $alter)); } } catch (Throwable $e) { // ignore (no permission, etc.) } if (empty($_SESSION['csrf'])) $_SESSION['csrf'] = bin2hex(random_bytes(32)); function csrf_ok($t): bool { return is_string($t) && hash_equals($_SESSION['csrf'], $t); } if (!function_exists('flash_set')) { function flash_set(string $type, string $msg): void { $_SESSION['_flash'] = ['type'=>$type,'msg'=>$msg]; } } if (!function_exists('flash_get')) { function flash_get(): ?array { $f=$_SESSION['_flash']??null; unset($_SESSION['_flash']); return is_array($f)?$f:null; } } $f = flash_get(); if ($_SERVER['REQUEST_METHOD'] === 'POST') { if (!csrf_ok((string)($_POST['csrf'] ?? ''))) { flash_set('danger', 'Bad CSRF token.'); header('Location: /public/admin/projects.php'); exit; } $action = (string)($_POST['action'] ?? ''); if ($action === 'create') { $title = trim((string)($_POST['title'] ?? '')); $tag = trim((string)($_POST['tag'] ?? '')); $year = trim((string)($_POST['year'] ?? '')); $summary = trim((string)($_POST['summary'] ?? '')); $shortSummary = trim((string)($_POST['short_summary'] ?? '')); $slug = trim((string)($_POST['slug'] ?? '')); $sort = (int)($_POST['sort_order'] ?? 0); $techCsv = trim((string)($_POST['tech_csv'] ?? '')); $tech = array_values(array_filter(array_map('trim', explode(',', $techCsv)))); $linksLines = preg_split("/\r\n|\n|\r/", (string)($_POST['links_lines'] ?? '')) ?: []; $links = []; foreach ($linksLines as $line) { $line = trim($line); if ($line === '') continue; // format: Label | https://url $parts = array_map('trim', explode('|', $line, 2)); $label = $parts[0] ?? ''; $url = $parts[1] ?? ''; if ($label !== '' && $url !== '') $links[] = ['label'=>$label,'url'=>$url]; } $slug = project_slugify($slug !== '' ? $slug : $title); if ($slug === '' && $title !== '') $slug = project_slugify($title); if ($title === '') { flash_set('danger', 'Title is required.'); } elseif ($slug === '' || !preg_match('/^[a-z0-9-]{2,120}$/', $slug)) { flash_set('danger', 'Slug must be letters, numbers, or hyphen (auto-derived from title).'); } elseif ($summary === '' || mb_strlen($summary) > 5000) { flash_set('danger', 'Summary must be 1-5000 characters.'); } elseif ($shortSummary !== '' && mb_strlen($shortSummary) > 240) { flash_set('danger', 'Card summary must be 240 characters or less.'); } else { $techJson = json_encode($tech, JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES); $linksJson = json_encode($links, JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES); $st = pdo()->prepare("SELECT id FROM projects WHERE slug = ? LIMIT 1"); $st->execute([$slug]); if ($st->fetch()) { flash_set('danger', 'Slug already in use.'); } else { pdo()->prepare("INSERT INTO projects (slug,title,tag,year,summary,short_summary,tech_json,links_json,sort_order) VALUES (?,?,?,?,?,?,?,?,?)") ->execute([$slug,$title,$tag,$year,$summary,$shortSummary ?: null,$techJson,$linksJson,$sort]); $newId = (int)pdo()->lastInsertId(); flash_set('success', 'Project added. Upload screenshots in the Gallery tab.'); $redirect = '/public/admin/project_edit.php'; if ($newId > 0) $redirect .= '?id=' . $newId . '&tab=gallery#tabGallery'; header('Location: ' . $redirect); exit; } } header('Location: /public/admin/projects.php'); exit; } if ($action === 'delete') { $id = (int)($_POST['id'] ?? 0); if ($id > 0) { pdo()->prepare("DELETE FROM projects WHERE id=?")->execute([$id]); flash_set('success', 'Project deleted.'); } header('Location: /public/admin/projects.php'); exit; } } $projects = []; try { $projects = pdo()->query("SELECT id, slug, title, tag, year, summary, tech_json, links_json, sort_order FROM projects ORDER BY sort_order ASC, id DESC")->fetchAll() ?: []; } catch (Throwable $e) {} $newParam = $_GET['new'] ?? null; $openNew = $newParam !== null; ?> Projects • Admin

Projects

Add New
Create a new project card
Save a project, then use its Gallery button.
Existing Projects
total
ID Title Year Tag Actions
Edit Gallery
No projects yet.