query("SHOW COLUMNS FROM skills")->fetchAll(PDO::FETCH_COLUMN, 0); $alter = []; if (!in_array('category', $cols, true)) $alter[] = "ADD COLUMN category VARCHAR(32) NOT NULL DEFAULT 'language'"; if (!in_array('parent_id', $cols, true)) $alter[] = "ADD COLUMN parent_id INT NULL DEFAULT NULL"; if ($alter) { pdo()->exec("ALTER TABLE skills " . implode(", ", $alter)); } } catch (Throwable $e) {} // ensure pivot table for multi-language frameworks try { pdo()->exec(" CREATE TABLE IF NOT EXISTS skill_links ( id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, framework_id INT NOT NULL, language_id INT NOT NULL, UNIQUE KEY uniq_fw_lang (framework_id, language_id), KEY idx_fw (framework_id), KEY idx_lang (language_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; "); // seed from legacy parent_id if any exist pdo()->exec(" INSERT IGNORE INTO skill_links (framework_id, language_id) SELECT id AS framework_id, parent_id AS language_id FROM skills WHERE category='framework' AND parent_id IS NOT NULL AND parent_id > 0 "); } catch (Throwable $e) {} 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(); $allowedCategories = ['language','framework']; function normalize_ids(array $ids): array { $out = []; foreach ($ids as $v) { $v = (int)$v; if ($v > 0) $out[$v] = $v; } return array_values($out); } function fetch_language_ids(): array { try { $ids = pdo()->query("SELECT id FROM skills WHERE category='language'")->fetchAll(PDO::FETCH_COLUMN, 0); return array_map('intval', $ids ?: []); } catch (Throwable $e) { return []; } } function sync_framework_languages(int $frameworkId, array $languageIds, array $validLanguageIds): void { $validSet = array_flip($validLanguageIds); $languageIds = array_values(array_filter(array_unique(array_map('intval', $languageIds)), function ($id) use ($validSet) { return $id > 0 && isset($validSet[$id]); })); pdo()->prepare("DELETE FROM skill_links WHERE framework_id=?")->execute([$frameworkId]); if (!$languageIds) return; $ins = pdo()->prepare("INSERT IGNORE INTO skill_links (framework_id, language_id) VALUES (?, ?)"); foreach ($languageIds as $lid) { $ins->execute([$frameworkId, $lid]); } } if ($_SERVER['REQUEST_METHOD'] === 'POST') { if (!csrf_ok((string)($_POST['csrf'] ?? ''))) { flash_set('danger', 'Bad CSRF token.'); header('Location: /public/admin/skills.php'); exit; } // DELETE (handled first, so it never falls into update_many) if (isset($_POST['delete_id'])) { $id = (int)($_POST['delete_id'] ?? 0); if ($id > 0) { pdo()->prepare("UPDATE skills SET parent_id=NULL WHERE parent_id=?")->execute([$id]); pdo()->prepare("DELETE FROM skill_links WHERE framework_id=? OR language_id=?")->execute([$id, $id]); pdo()->prepare("DELETE FROM skills WHERE id=?")->execute([$id]); flash_set('success', 'Skill deleted.'); } header('Location: /public/admin/skills.php'); exit; } $action = (string)($_POST['action'] ?? ''); if ($action === 'create') { $label = trim((string)($_POST['label'] ?? '')); $icon = trim((string)($_POST['icon'] ?? 'ri-code-s-slash-line')); $level = max(0, min(100, (int)($_POST['level'] ?? 0))); $sort = (int)($_POST['sort_order'] ?? 0); $category = strtolower(trim((string)($_POST['category'] ?? 'language'))); if (!in_array($category, $allowedCategories, true)) $category = 'language'; $parentIds = $category === 'framework' ? normalize_ids((array)($_POST['parent_id_new'] ?? [])) : []; $validLanguageIds = $category === 'framework' ? fetch_language_ids() : []; if ($parentIds && $validLanguageIds) { $parentIds = array_values(array_intersect($parentIds, $validLanguageIds)); } if ($label === '') { flash_set('danger', 'Label is required.'); } else { if ($category === 'framework') { if (!$parentIds) { flash_set('danger', 'Select one or more parent languages for this framework.'); header('Location: /public/admin/skills.php'); exit; } } else { $parentIds = []; } $primaryParent = ($category === 'framework' && $parentIds) ? (int)$parentIds[0] : null; pdo()->prepare("INSERT INTO skills (label, icon, level, sort_order, category, parent_id) VALUES (?,?,?,?,?,?)") ->execute([$label, $icon, $level, $sort, $category, $primaryParent]); $newId = (int)pdo()->lastInsertId(); if ($category === 'framework' && $newId > 0) { sync_framework_languages($newId, $parentIds, $validLanguageIds); } flash_set('success', 'Skill added.'); } header('Location: /public/admin/skills.php'); exit; } if ($action === 'update_many') { $ids = $_POST['id'] ?? []; $categoriesMap = []; foreach ($ids as $i => $idRaw) { $cid = (int)$idRaw; $catRaw = strtolower(trim((string)($_POST['category'][$i] ?? 'language'))); $categoriesMap[$cid] = in_array($catRaw, $allowedCategories, true) ? $catRaw : 'language'; } // use DB-backed language ids to avoid losing links if a row is flipped to framework $validLanguageIds = fetch_language_ids(); $parentSelections = (array)($_POST['parent_id'] ?? []); foreach ($ids as $i => $idRaw) { $id = (int)$idRaw; if ($id <= 0) continue; $label = trim((string)($_POST['label'][$i] ?? '')); $icon = trim((string)($_POST['icon'][$i] ?? 'ri-code-s-slash-line')); $level = max(0, min(100, (int)($_POST['level'][$i] ?? 0))); $sort = (int)($_POST['sort_order'][$i] ?? 0); $category = $categoriesMap[$id] ?? 'language'; $selectedParents = ($category === 'framework') ? normalize_ids((array)($parentSelections[$id] ?? [])) : []; if ($selectedParents && $validLanguageIds) { $selectedParents = array_values(array_intersect($selectedParents, $validLanguageIds)); } $primaryParent = ($category === 'framework' && $selectedParents) ? (int)$selectedParents[0] : null; pdo()->prepare("UPDATE skills SET label=?, icon=?, level=?, sort_order=?, category=?, parent_id=? WHERE id=?") ->execute([$label, $icon, $level, $sort, $category, $primaryParent, $id]); if ($category === 'framework') { sync_framework_languages($id, $selectedParents, $validLanguageIds); // if this was previously a language, clear stale links where it was used as a parent pdo()->prepare("DELETE FROM skill_links WHERE language_id=? AND framework_id!=?")->execute([$id, $id]); } else { // if it is (or is now) a language, only remove links where it was the framework pdo()->prepare("DELETE FROM skill_links WHERE framework_id=?")->execute([$id]); } } flash_set('success', 'Skills updated.'); header('Location: /public/admin/skills.php'); exit; } } $skills = []; try { $skills = pdo()->query("SELECT id,label,icon,level,sort_order,category,parent_id FROM skills ORDER BY (category='language') DESC, sort_order ASC, id ASC")->fetchAll() ?: []; } catch (Throwable $e) {} $skillLinks = []; try { $skillLinks = pdo()->query("SELECT framework_id, language_id FROM skill_links")->fetchAll(PDO::FETCH_ASSOC) ?: []; } catch (Throwable $e) {} $frameworkParentMap = []; foreach ($skillLinks as $link) { $fw = (int)($link['framework_id'] ?? 0); $lang = (int)($link['language_id'] ?? 0); if ($fw > 0 && $lang > 0) { if (!isset($frameworkParentMap[$fw])) $frameworkParentMap[$fw] = []; $frameworkParentMap[$fw][$lang] = $lang; } } $languageOptions = array_values(array_filter($skills, function ($s) { return strtolower((string)($s['category'] ?? 'language')) === 'language'; })); $languageLabelMap = []; foreach ($languageOptions as $lang) { $languageLabelMap[(int)$lang['id']] = (string)$lang['label']; } // fallback to legacy parent_id if no links saved yet foreach ($skills as $s) { $cat = strtolower((string)($s['category'] ?? 'language')); if ($cat !== 'framework') continue; $fwId = (int)($s['id'] ?? 0); $pid = (int)($s['parent_id'] ?? 0); if ($fwId > 0 && $pid > 0 && empty($frameworkParentMap[$fwId])) { $frameworkParentMap[$fwId][$pid] = $pid; } } $frameworkParentMap = array_map('array_values', $frameworkParentMap); $openNew = isset($_GET['new']); ?> Skills • Admin

Tech Stack (%)

Create languages and nest frameworks beneath them.
Add New
Create a new bar on the homepage
Choose one or more languages to nest its frameworks.
Edit Skills
total
ID Label Icon Category Parents % Sort Delete
No skills yet.