522 lines
22 KiB
PHP
522 lines
22 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
require __DIR__ . '/../../includes/bootstrap.php';
|
|
|
|
require_admin_login();
|
|
|
|
// ensure schema supports categories + parent relation
|
|
try {
|
|
$cols = pdo()->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']);
|
|
?>
|
|
<!doctype html>
|
|
<html>
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
<title>Skills • Admin</title>
|
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
<link rel="stylesheet" href="/public/css/admin.css">
|
|
<style>
|
|
.lvl{width:120px}
|
|
body { font-family: 'Inter', 'Segoe UI', system-ui, -apple-system, sans-serif; }
|
|
.table-responsive { overflow: visible; }
|
|
.parent-wrap { position: relative; }
|
|
.new-skill-card { position: relative; z-index: 50; overflow: visible; }
|
|
.parent-picker-btn {
|
|
background: linear-gradient(120deg, rgba(255,255,255,.05), rgba(139,92,246,.08));
|
|
border: 1px solid rgba(255,255,255,.08);
|
|
color: #f8fafc;
|
|
transition: all .18s ease;
|
|
}
|
|
.parent-picker-btn:hover {
|
|
border-color: rgba(167,139,250,.6);
|
|
box-shadow: 0 12px 28px rgba(0,0,0,.2);
|
|
}
|
|
.parent-summary {
|
|
font-weight: 600;
|
|
letter-spacing: 0.01em;
|
|
}
|
|
.parent-menu {
|
|
max-height: 260px;
|
|
overflow-y: auto;
|
|
box-shadow: 0 18px 38px rgba(0,0,0,.3);
|
|
z-index: 5000;
|
|
position: absolute;
|
|
inset: auto auto auto 0;
|
|
min-width: 100%;
|
|
}
|
|
.parent-option {
|
|
padding: 8px 10px;
|
|
border-radius: 10px;
|
|
transition: background .15s ease, border-color .15s ease;
|
|
border: 1px solid transparent;
|
|
}
|
|
.parent-option:hover {
|
|
background: rgba(255,255,255,.05);
|
|
border-color: rgba(255,255,255,.1);
|
|
}
|
|
</style>
|
|
</head>
|
|
<body class="min-vh-100">
|
|
<div class="container py-4" style="max-width: 1100px">
|
|
|
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
|
<div>
|
|
<h1 class="h3 m-0">Tech Stack (%)</h1>
|
|
<div class="text-white-50 small">Create languages and nest frameworks beneath them.</div>
|
|
</div>
|
|
<div class="d-flex gap-2">
|
|
<a class="btn btn-outline-light" href="/public/admin/dashboard.php">Dashboard</a>
|
|
<a class="btn btn-outline-light" href="/public/admin/logout.php">Logout</a>
|
|
</div>
|
|
</div>
|
|
|
|
<?php if ($f): ?>
|
|
<div class="alert alert-<?= htmlspecialchars($f['type']) ?>"><?= htmlspecialchars($f['msg']) ?></div>
|
|
<?php endif; ?>
|
|
|
|
<div class="card card-glass p-3 mb-3 new-skill-card">
|
|
<div class="d-flex justify-content-between align-items-center">
|
|
<div>
|
|
<div class="text-white-50 small">Add New</div>
|
|
<div class="fw-bold">Create a new bar on the homepage</div>
|
|
</div>
|
|
<button class="btn btn-violet" type="button" data-scroll-target="#newSkill">
|
|
Add Skill
|
|
</button>
|
|
</div>
|
|
|
|
<div class="mt-3" id="newSkill">
|
|
<form method="post" class="row g-3">
|
|
<input type="hidden" name="csrf" value="<?= htmlspecialchars($_SESSION['csrf']) ?>">
|
|
<input type="hidden" name="action" value="create">
|
|
|
|
<div class="col-md-5">
|
|
<label class="form-label text-white-50">Label</label>
|
|
<input class="form-control" name="label" required maxlength="120" placeholder="PHP / Laravel">
|
|
</div>
|
|
<div class="col-md-3">
|
|
<label class="form-label text-white-50">Icon (RemixIcon class)</label>
|
|
<input class="form-control" name="icon" maxlength="80" value="ri-code-s-slash-line">
|
|
</div>
|
|
<div class="col-md-2">
|
|
<label class="form-label text-white-50">%</label>
|
|
<input class="form-control" name="level" type="number" min="0" max="100" placeholder="100">
|
|
</div>
|
|
<div class="col-md-1">
|
|
<label class="form-label text-white-50">Sort</label>
|
|
<input class="form-control" name="sort_order" type="number" value="0">
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label class="form-label text-white-50">Category</label>
|
|
<select class="form-select" name="category" data-parent-target="#parent-new-wrap">
|
|
<option value="language">Language</option>
|
|
<option value="framework">Framework</option>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-8">
|
|
<div class="parent-wrap d-none" data-parent-wrap id="parent-new-wrap">
|
|
<label class="form-label text-white-50">Parent languages (for frameworks)</label>
|
|
<div class="dropdown w-100">
|
|
<button class="btn parent-picker-btn w-100 d-flex justify-content-between align-items-center" type="button" data-bs-toggle="dropdown" data-bs-auto-close="outside" data-bs-display="static">
|
|
<span class="parent-summary" data-parent-summary>Select parent languages</span>
|
|
<i class="ri-arrow-down-s-line ms-2"></i>
|
|
</button>
|
|
<div class="dropdown-menu dropdown-menu-dark w-100 p-2 parent-menu">
|
|
<?php foreach ($languageOptions as $lang): ?>
|
|
<label class="parent-option d-flex align-items-center gap-2 text-white-75 small mb-1">
|
|
<input class="form-check-input" type="checkbox" name="parent_id_new[]" value="<?= (int)$lang['id'] ?>" data-label="<?= htmlspecialchars((string)$lang['label']) ?>">
|
|
<span><?= htmlspecialchars((string)$lang['label']) ?></span>
|
|
</label>
|
|
<?php endforeach; ?>
|
|
</div>
|
|
</div>
|
|
<div class="form-text text-white-50 mt-1">Choose one or more languages to nest its frameworks.</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-12">
|
|
<button class="btn btn-violet">Save Skill</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card card-glass p-0 overflow-hidden">
|
|
<div class="p-3 border-bottom border-white/10 d-flex justify-content-between align-items-center">
|
|
<div class="fw-bold">Edit Skills</div>
|
|
<div class="text-white-50 small"><?= count($skills) ?> total</div>
|
|
</div>
|
|
|
|
<!-- ONE form only -->
|
|
<form method="post">
|
|
<input type="hidden" name="csrf" value="<?= htmlspecialchars($_SESSION['csrf']) ?>">
|
|
<input type="hidden" name="action" value="update_many">
|
|
|
|
<div class="table-responsive">
|
|
<table class="table table-dark table-hover mb-0 align-middle">
|
|
<thead>
|
|
<tr>
|
|
<th style="width:70px">ID</th>
|
|
<th>Label</th>
|
|
<th style="width:200px">Icon</th>
|
|
<th style="width:140px">Category</th>
|
|
<th style="width:240px">Parents</th>
|
|
<th class="lvl">%</th>
|
|
<th style="width:90px">Sort</th>
|
|
<th style="width:110px">Delete</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php foreach ($skills as $s): ?>
|
|
<tr>
|
|
<td class="text-white-50">
|
|
<?= (int)$s['id'] ?>
|
|
<input type="hidden" name="id[]" value="<?= (int)$s['id'] ?>">
|
|
</td>
|
|
<td><input class="form-control" name="label[]" value="<?= htmlspecialchars((string)$s['label']) ?>" maxlength="120"></td>
|
|
<td><input class="form-control" name="icon[]" value="<?= htmlspecialchars((string)$s['icon']) ?>" maxlength="80"></td>
|
|
<td>
|
|
<?php $cat = strtolower((string)($s['category'] ?? 'language')); ?>
|
|
<select class="form-select" name="category[]" data-parent-target="#parent-<?= (int)$s['id'] ?>-wrap">
|
|
<option value="language" <?= $cat === 'language' ? 'selected' : '' ?>>Language</option>
|
|
<option value="framework" <?= $cat === 'framework' ? 'selected' : '' ?>>Framework</option>
|
|
</select>
|
|
</td>
|
|
<td>
|
|
<?php $selectedParents = $frameworkParentMap[(int)$s['id']] ?? []; ?>
|
|
<?php
|
|
$summaryText = 'Select parent languages';
|
|
$labels = [];
|
|
foreach ($selectedParents as $pid) {
|
|
if (isset($languageLabelMap[$pid])) $labels[] = $languageLabelMap[$pid];
|
|
}
|
|
if ($labels) $summaryText = implode(', ', $labels);
|
|
?>
|
|
<div class="parent-wrap <?= $cat === 'framework' ? '' : 'd-none' ?>" data-parent-wrap id="parent-<?= (int)$s['id'] ?>-wrap">
|
|
<div class="dropdown w-100">
|
|
<button class="btn parent-picker-btn w-100 d-flex justify-content-between align-items-center" type="button" data-bs-toggle="dropdown" data-bs-auto-close="outside" data-bs-display="static">
|
|
<span class="parent-summary" data-parent-summary><?= htmlspecialchars($summaryText) ?></span>
|
|
<i class="ri-arrow-down-s-line ms-2"></i>
|
|
</button>
|
|
<div class="dropdown-menu dropdown-menu-dark w-100 p-2 parent-menu">
|
|
<?php foreach ($languageOptions as $lang): ?>
|
|
<?php if ((int)$lang['id'] === (int)$s['id']) continue; ?>
|
|
<label class="parent-option d-flex align-items-center gap-2 text-white-75 small mb-1">
|
|
<input
|
|
class="form-check-input"
|
|
type="checkbox"
|
|
name="parent_id[<?= (int)$s['id'] ?>][]"
|
|
value="<?= (int)$lang['id'] ?>"
|
|
data-label="<?= htmlspecialchars((string)$lang['label']) ?>"
|
|
<?= in_array((int)$lang['id'], $selectedParents, true) ? 'checked' : '' ?>
|
|
>
|
|
<span><?= htmlspecialchars((string)$lang['label']) ?></span>
|
|
</label>
|
|
<?php endforeach; ?>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
<td><input class="form-control" name="level[]" type="number" min="0" max="100" value="<?= (int)$s['level'] ?>"></td>
|
|
<td><input class="form-control" name="sort_order[]" type="number" value="<?= (int)$s['sort_order'] ?>"></td>
|
|
<td>
|
|
<!-- NO inner form -->
|
|
<button
|
|
class="btn btn-outline-danger btn-sm"
|
|
type="submit"
|
|
name="delete_id"
|
|
value="<?= (int)$s['id'] ?>"
|
|
data-confirm="Delete this skill?"
|
|
>Del</button>
|
|
</td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
|
|
<?php if (!$skills): ?>
|
|
<tr><td colspan="8" class="text-center text-white-50 py-4">No skills yet.</td></tr>
|
|
<?php endif; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<div class="p-3 border-top border-white/10">
|
|
<button class="btn btn-violet">Save All Changes</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
|
<script>
|
|
(function ($) {
|
|
$(function () {
|
|
$('[data-scroll-target]').on('click', function () {
|
|
const target = $(this).data('scroll-target');
|
|
if (!target) return;
|
|
const $dest = $(target);
|
|
if ($dest.length) {
|
|
$('html, body').animate({ scrollTop: $dest.offset().top - 20 }, 250);
|
|
}
|
|
});
|
|
|
|
const syncSummary = ($wrap) => {
|
|
const labels = $wrap
|
|
.find('input[type="checkbox"]:checked')
|
|
.map(function () { return $(this).data("label") || $(this).val(); })
|
|
.get();
|
|
$wrap.find('[data-parent-summary]').text(labels.length ? labels.join(', ') : 'Select parent languages');
|
|
};
|
|
|
|
const updateParentVisibility = ($select) => {
|
|
const targetSelector = $select.data('parent-target');
|
|
if (!targetSelector) return;
|
|
const $wrap = $(targetSelector);
|
|
if (!$wrap.length) return;
|
|
|
|
const show = $select.val() === 'framework';
|
|
$wrap.toggleClass('d-none', !show);
|
|
if (!show) {
|
|
$wrap.find('input[type="checkbox"]').prop('checked', false);
|
|
$wrap.find('select').each(function () {
|
|
$(this).find('option').prop('selected', false);
|
|
});
|
|
}
|
|
syncSummary($wrap);
|
|
};
|
|
|
|
$('[data-parent-target]').each(function () {
|
|
const $select = $(this);
|
|
updateParentVisibility($select);
|
|
$select.on('change', () => updateParentVisibility($select));
|
|
});
|
|
|
|
$('.parent-wrap').each(function () {
|
|
const $wrap = $(this);
|
|
$wrap.find('input[type="checkbox"]').on('change', () => syncSummary($wrap));
|
|
syncSummary($wrap);
|
|
});
|
|
});
|
|
})(jQuery);
|
|
</script>
|
|
</body>
|
|
</html>
|