475 lines
19 KiB
JavaScript
475 lines
19 KiB
JavaScript
|
|
const tools = [
|
|||
|
|
{id: "tool_1", name: "ClipFlow", category: "开发效率", description: "轻量级代码片段管理工具,支持团队共享与快速插入。", tags: ["代码片段", "团队协作", "VSCode"], version: "2.4.1", size: "12.3 MB", downloads: 36850, updatedAt: "2026-03-12", features: ["多工作区同步", "标签归档", "快捷键模板插入"]},
|
|||
|
|
{id: "tool_2", name: "TaskOrbit", category: "团队协作", description: "面向产品团队的任务看板与冲刺管理工具。", tags: ["看板", "项目管理", "甘特图"], version: "1.9.0", url: "https://example.com/tools/taskorbit", downloads: 29210, updatedAt: "2026-03-18", features: ["多视图任务管理", "冲刺模板", "成员工作负载分析"]},
|
|||
|
|
{id: "tool_3", name: "PixelLint", category: "设计协作", description: "设计稿一致性检查工具,可自动扫描颜色与间距规范。", tags: ["设计规范", "Figma", "UI 质检"], version: "3.1.2", size: "7.8 MB", downloads: 21430, updatedAt: "2026-02-25", features: ["组件一致性对比", "批量标注问题", "导出质检报告"]},
|
|||
|
|
{id: "tool_4", name: "DataSparrow", category: "数据分析", description: "可视化数据清洗与探索平台,适合中小团队快速上手。", tags: ["可视化", "数据清洗", "BI"], version: "4.0.0", url: "https://example.com/tools/datasparrow", downloads: 17680, updatedAt: "2026-03-21", features: ["拖拽式数据流", "字段质量检测", "图表模板库"]},
|
|||
|
|
{id: "tool_5", name: "ShipMate", category: "自动化", description: "一键打包发布脚本管理器,统一多环境部署流程。", tags: ["CI/CD", "部署", "脚本"], version: "2.0.3", size: "10.1 MB", downloads: 25600, updatedAt: "2026-03-09", features: ["多环境变量模板", "发布回滚", "构建流水线监控"]},
|
|||
|
|
{id: "tool_6", name: "InsightPanel", category: "数据分析", description: "业务指标仪表盘构建器,支持实时数据看板。", tags: ["仪表盘", "指标", "实时看板"], version: "5.2.1", url: "https://example.com/tools/insightpanel", downloads: 40980, updatedAt: "2026-03-20", features: ["实时刷新组件", "阈值告警", "多数据源连接"]},
|
|||
|
|
{id: "tool_7", name: "CloudWatchdog", category: "运维监控", description: "基础设施健康监控工具,支持可视化告警规则。", tags: ["告警", "监控", "日志分析"], version: "1.6.8", size: "15.4 MB", downloads: 19870, updatedAt: "2026-03-15", features: ["阈值策略库", "异常聚合", "告警分级通知"]},
|
|||
|
|
{id: "tool_8", name: "FormForge", category: "开发效率", description: "表单构建器,支持低代码生成校验规则与提交流程。", tags: ["低代码", "表单", "校验"], version: "3.7.5", size: "9.2 MB", downloads: 23200, updatedAt: "2026-03-11", features: ["可视化表单设计", "字段联动", "提交数据导出"]},
|
|||
|
|
{id: "tool_9", name: "QuerySprint", category: "数据分析", description: "面向分析师的 SQL 协作平台,支持查询片段共享。", tags: ["SQL", "查询优化", "协作"], version: "2.8.4", url: "https://example.com/tools/querysprint", downloads: 27540, updatedAt: "2026-02-18", features: ["查询版本管理", "性能诊断建议", "团队模版库"]},
|
|||
|
|
{id: "tool_10", name: "TestPilot", category: "自动化", description: "自动化测试流程编排工具,支持 API 与 UI 混合测试。", tags: ["自动化测试", "API", "回归测试"], version: "4.3.0", size: "22.4 MB", downloads: 31420, updatedAt: "2026-03-17", features: ["测试用例可视化", "失败重跑策略", "测试报告仪表盘"]},
|
|||
|
|
{id: "tool_11", name: "BrandBoard", category: "设计协作", description: "品牌资产管理工具,统一素材规范与组件资产。", tags: ["品牌资产", "设计系统", "素材管理"], version: "1.4.2", url: "https://example.com/tools/brandboard", downloads: 16890, updatedAt: "2026-03-08", features: ["版本化素材库", "品牌规范手册", "跨团队共享链接"]},
|
|||
|
|
{id: "tool_12", name: "DeployLens", category: "运维监控", description: "发布质量追踪平台,聚合版本、错误率与回滚记录。", tags: ["发布追踪", "SRE", "质量分析"], version: "2.2.6", url: "https://example.com/tools/deploylens", downloads: 22160, updatedAt: "2026-03-19", features: ["发布健康指标", "回滚影响分析", "问题根因视图"]}
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
const state = {query: "", category: "all", sortBy: "popular", page: 1, pageSize: 6};
|
|||
|
|
const keywords = ["自动化", "设计系统", "仪表盘", "监控", "协作"];
|
|||
|
|
let toastTimer = null;
|
|||
|
|
|
|||
|
|
const elements = {
|
|||
|
|
headerWrap: document.querySelector(".header-wrap"),
|
|||
|
|
overviewBtn: document.getElementById("overviewBtn"),
|
|||
|
|
searchInput: document.getElementById("searchInput"),
|
|||
|
|
categorySelect: document.getElementById("categorySelect"),
|
|||
|
|
categorySidebarList: document.getElementById("categorySidebarList"),
|
|||
|
|
sortSelect: document.getElementById("sortSelect"),
|
|||
|
|
resetBtn: document.getElementById("resetBtn"),
|
|||
|
|
hotKeywords: document.getElementById("hotKeywords"),
|
|||
|
|
resultTip: document.getElementById("resultTip"),
|
|||
|
|
toolGrid: document.getElementById("toolGrid"),
|
|||
|
|
pagination: document.getElementById("pagination"),
|
|||
|
|
prevBtn: document.getElementById("prevBtn"),
|
|||
|
|
nextBtn: document.getElementById("nextBtn"),
|
|||
|
|
pageText: document.getElementById("pageText"),
|
|||
|
|
detailModal: document.getElementById("detailModal"),
|
|||
|
|
closeModalBtn: document.getElementById("closeModalBtn"),
|
|||
|
|
overviewModal: document.getElementById("overviewModal"),
|
|||
|
|
closeOverviewModalBtn: document.getElementById("closeOverviewModalBtn"),
|
|||
|
|
detailTitle: document.getElementById("detailTitle"),
|
|||
|
|
detailDescription: document.getElementById("detailDescription"),
|
|||
|
|
detailMeta: document.getElementById("detailMeta"),
|
|||
|
|
detailFeatures: document.getElementById("detailFeatures"),
|
|||
|
|
toast: document.getElementById("toast"),
|
|||
|
|
kpiTotal: document.getElementById("kpiTotal"),
|
|||
|
|
kpiCategories: document.getElementById("kpiCategories"),
|
|||
|
|
kpiDownloads: document.getElementById("kpiDownloads"),
|
|||
|
|
kpiFiltered: document.getElementById("kpiFiltered")
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
function escapeHtml(value) {
|
|||
|
|
return value
|
|||
|
|
.replace(/&/g, "&")
|
|||
|
|
.replace(/</g, "<")
|
|||
|
|
.replace(/>/g, ">")
|
|||
|
|
.replace(/"/g, """)
|
|||
|
|
.replace(/'/g, "'");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function formatNumber(value) {
|
|||
|
|
return new Intl.NumberFormat("zh-CN").format(value);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function formatDate(dateText) {
|
|||
|
|
return new Intl.DateTimeFormat("zh-CN", {year: "numeric", month: "2-digit", day: "2-digit"})
|
|||
|
|
.format(new Date(dateText));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function getCategories() {
|
|||
|
|
return Array.from(new Set(tools.map((tool) => tool.category)));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function matchesQuery(tool, query) {
|
|||
|
|
if (!query) {
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
const pool = [tool.name, tool.description, tool.category, ...tool.tags].join(" ").toLowerCase();
|
|||
|
|
return pool.includes(query);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function buildOptions() {
|
|||
|
|
const categories = getCategories();
|
|||
|
|
categories.forEach((category) => {
|
|||
|
|
const option = document.createElement("option");
|
|||
|
|
option.value = category;
|
|||
|
|
option.textContent = category;
|
|||
|
|
elements.categorySelect.append(option);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
keywords.forEach((keyword) => {
|
|||
|
|
const button = document.createElement("button");
|
|||
|
|
button.type = "button";
|
|||
|
|
button.className = "chip";
|
|||
|
|
button.dataset.keyword = keyword;
|
|||
|
|
button.textContent = keyword;
|
|||
|
|
elements.hotKeywords.append(button);
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function renderCategorySidebar() {
|
|||
|
|
if (!elements.categorySidebarList) {
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const query = state.query.trim().toLowerCase();
|
|||
|
|
const queryMatchedTools = tools.filter((tool) => matchesQuery(tool, query));
|
|||
|
|
const countMap = new Map();
|
|||
|
|
queryMatchedTools.forEach((tool) => {
|
|||
|
|
countMap.set(tool.category, (countMap.get(tool.category) || 0) + 1);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
const items = [
|
|||
|
|
{value: "all", label: "全部分类", count: queryMatchedTools.length},
|
|||
|
|
...getCategories().map((category) => ({
|
|||
|
|
value: category,
|
|||
|
|
label: category,
|
|||
|
|
count: countMap.get(category) || 0
|
|||
|
|
}))
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
elements.categorySidebarList.textContent = "";
|
|||
|
|
const fragment = document.createDocumentFragment();
|
|||
|
|
|
|||
|
|
items.forEach((item) => {
|
|||
|
|
const button = document.createElement("button");
|
|||
|
|
button.type = "button";
|
|||
|
|
button.className = "category-side-btn";
|
|||
|
|
button.dataset.category = item.value;
|
|||
|
|
button.setAttribute("aria-pressed", item.value === state.category ? "true" : "false");
|
|||
|
|
if (item.value === state.category) {
|
|||
|
|
button.classList.add("active");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const label = document.createElement("span");
|
|||
|
|
label.className = "label";
|
|||
|
|
label.textContent = item.label;
|
|||
|
|
|
|||
|
|
const count = document.createElement("span");
|
|||
|
|
count.className = "count";
|
|||
|
|
count.textContent = formatNumber(item.count);
|
|||
|
|
|
|||
|
|
button.append(label, count);
|
|||
|
|
fragment.append(button);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
elements.categorySidebarList.append(fragment);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function filterTools() {
|
|||
|
|
const query = state.query.trim().toLowerCase();
|
|||
|
|
const filtered = tools.filter((tool) => {
|
|||
|
|
if (state.category !== "all" && state.category !== tool.category) {
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
return matchesQuery(tool, query);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
const sorted = [...filtered];
|
|||
|
|
if (state.sortBy === "popular") {
|
|||
|
|
sorted.sort((a, b) => b.downloads - a.downloads);
|
|||
|
|
} else if (state.sortBy === "latest") {
|
|||
|
|
sorted.sort((a, b) => new Date(b.updatedAt) - new Date(a.updatedAt));
|
|||
|
|
} else {
|
|||
|
|
sorted.sort((a, b) => a.name.localeCompare(b.name, "zh-Hans-CN"));
|
|||
|
|
}
|
|||
|
|
return sorted;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function paginate(items) {
|
|||
|
|
const totalPages = Math.max(1, Math.ceil(items.length / state.pageSize));
|
|||
|
|
if (state.page > totalPages) {
|
|||
|
|
state.page = totalPages;
|
|||
|
|
}
|
|||
|
|
const start = (state.page - 1) * state.pageSize;
|
|||
|
|
return {items: items.slice(start, start + state.pageSize), totalPages, start};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function renderKpi(filteredCount) {
|
|||
|
|
elements.kpiTotal.textContent = formatNumber(tools.length);
|
|||
|
|
elements.kpiCategories.textContent = formatNumber(new Set(tools.map((tool) => tool.category)).size);
|
|||
|
|
elements.kpiDownloads.textContent = formatNumber(tools.reduce((sum, tool) => sum + tool.downloads, 0));
|
|||
|
|
elements.kpiFiltered.textContent = formatNumber(filteredCount);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function render() {
|
|||
|
|
const filtered = filterTools();
|
|||
|
|
const page = paginate(filtered);
|
|||
|
|
const displayStart = filtered.length ? page.start + 1 : 0;
|
|||
|
|
const displayEnd = Math.min(page.start + state.pageSize, filtered.length);
|
|||
|
|
|
|||
|
|
renderKpi(filtered.length);
|
|||
|
|
renderCategorySidebar();
|
|||
|
|
elements.resultTip.textContent = `共找到 ${formatNumber(filtered.length)} 个工具,当前显示 ${displayStart}-${displayEnd}。`;
|
|||
|
|
|
|||
|
|
if (filtered.length === 0) {
|
|||
|
|
elements.toolGrid.innerHTML = `
|
|||
|
|
<div class="empty">
|
|||
|
|
<p>没有匹配结果,请尝试更换关键词或分类。</p>
|
|||
|
|
<button id="clearEmptyBtn" type="button" class="btn">清空筛选条件</button>
|
|||
|
|
</div>
|
|||
|
|
`;
|
|||
|
|
elements.pagination.style.display = "none";
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
elements.toolGrid.innerHTML = page.items.map((tool, index) => `
|
|||
|
|
<article class="card" style="--stagger:${index * 45}ms;">
|
|||
|
|
<div class="card-top">
|
|||
|
|
<span class="category">${escapeHtml(tool.category)}</span>
|
|||
|
|
</div>
|
|||
|
|
<h3>${escapeHtml(tool.name)}</h3>
|
|||
|
|
<p class="desc">${escapeHtml(tool.description)}</p>
|
|||
|
|
<div class="tags">${tool.tags.map((tag) => `<span class="tag">${escapeHtml(tag)}</span>`).join("")}</div>
|
|||
|
|
<ul class="meta-list">
|
|||
|
|
<li>版本:<strong>${escapeHtml(tool.version)}</strong></li>
|
|||
|
|
${tool.size ? `<li>大小:<strong>${escapeHtml(tool.size)}</strong></li>` : ""}
|
|||
|
|
<li>更新:<strong>${formatDate(tool.updatedAt)}</strong></li>
|
|||
|
|
</ul>
|
|||
|
|
<div class="card-foot">
|
|||
|
|
<span class="download-num">${tool.url ? "访问" : "下载"} ${formatNumber(tool.downloads)}</span>
|
|||
|
|
<div class="actions">
|
|||
|
|
<button type="button" class="btn-small js-detail" data-id="${tool.id}">详情</button>
|
|||
|
|
${tool.url
|
|||
|
|
? `<button type="button" class="btn-small btn-open js-open" data-id="${tool.id}">打开网页</button>`
|
|||
|
|
: `<button type="button" class="btn-small btn-download js-download" data-id="${tool.id}">下载</button>`}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</article>
|
|||
|
|
`).join("");
|
|||
|
|
|
|||
|
|
elements.pagination.style.display = "flex";
|
|||
|
|
elements.pageText.textContent = `第 ${state.page} / ${page.totalPages} 页`;
|
|||
|
|
elements.prevBtn.disabled = state.page === 1;
|
|||
|
|
elements.nextBtn.disabled = state.page === page.totalPages;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function showToast(message) {
|
|||
|
|
elements.toast.textContent = message;
|
|||
|
|
elements.toast.classList.add("show");
|
|||
|
|
clearTimeout(toastTimer);
|
|||
|
|
toastTimer = setTimeout(() => elements.toast.classList.remove("show"), 2200);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function downloadTool(tool) {
|
|||
|
|
if (tool.url) {
|
|||
|
|
openWebTool(tool);
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const payload = {
|
|||
|
|
toolId: tool.id,
|
|||
|
|
toolName: tool.name,
|
|||
|
|
version: tool.version,
|
|||
|
|
category: tool.category,
|
|||
|
|
downloadedAt: new Date().toISOString()
|
|||
|
|
};
|
|||
|
|
const blob = new Blob([JSON.stringify(payload, null, 2)], {type: "application/json;charset=utf-8"});
|
|||
|
|
const url = URL.createObjectURL(blob);
|
|||
|
|
const a = document.createElement("a");
|
|||
|
|
a.href = url;
|
|||
|
|
a.download = `${tool.name.replace(/\s+/g, "-").toLowerCase()}-manifest.json`;
|
|||
|
|
document.body.append(a);
|
|||
|
|
a.click();
|
|||
|
|
a.remove();
|
|||
|
|
URL.revokeObjectURL(url);
|
|||
|
|
tool.downloads += 1;
|
|||
|
|
render();
|
|||
|
|
showToast(`${tool.name} 下载已开始(Mock)`);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function openWebTool(tool) {
|
|||
|
|
if (!tool.url) {
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
const nextWindow = window.open(tool.url, "_blank", "noopener,noreferrer");
|
|||
|
|
if (!nextWindow) {
|
|||
|
|
showToast("浏览器阻止了新窗口,请允许弹窗后重试");
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
tool.downloads += 1;
|
|||
|
|
render();
|
|||
|
|
showToast(`${tool.name} 已在新标签页打开`);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function openModal(tool) {
|
|||
|
|
elements.detailTitle.textContent = tool.name;
|
|||
|
|
elements.detailDescription.textContent = tool.description;
|
|||
|
|
elements.detailMeta.innerHTML = `
|
|||
|
|
<li>分类:<strong>${escapeHtml(tool.category)}</strong></li>
|
|||
|
|
<li>版本:<strong>${escapeHtml(tool.version)}</strong></li>
|
|||
|
|
${tool.size ? `<li>大小:<strong>${escapeHtml(tool.size)}</strong></li>` : ""}
|
|||
|
|
<li>访问方式:<strong>${tool.url ? "网页打开" : "下载安装"}</strong></li>
|
|||
|
|
<li>${tool.url ? "访问" : "下载"}:<strong>${formatNumber(tool.downloads)}</strong></li>
|
|||
|
|
<li>更新时间:<strong>${formatDate(tool.updatedAt)}</strong></li>
|
|||
|
|
`;
|
|||
|
|
elements.detailFeatures.innerHTML = tool.features.map((feature) => `<li>${escapeHtml(feature)}</li>`).join("");
|
|||
|
|
elements.detailModal.classList.add("open");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function closeModal() {
|
|||
|
|
elements.detailModal.classList.remove("open");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function openOverviewModal() {
|
|||
|
|
elements.overviewBtn.classList.add("active");
|
|||
|
|
elements.overviewBtn.setAttribute("aria-expanded", "true");
|
|||
|
|
elements.overviewModal.classList.add("open");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function closeOverviewModal() {
|
|||
|
|
elements.overviewBtn.classList.remove("active");
|
|||
|
|
elements.overviewBtn.setAttribute("aria-expanded", "false");
|
|||
|
|
elements.overviewModal.classList.remove("open");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function updateHeaderScrollState() {
|
|||
|
|
if (!elements.headerWrap) {
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
const isScrolled = window.scrollY > 8;
|
|||
|
|
elements.headerWrap.classList.toggle("is-scrolled", isScrolled);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function clearChipActive() {
|
|||
|
|
elements.hotKeywords.querySelectorAll(".chip").forEach((chip) => chip.classList.remove("active"));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function resetFilters() {
|
|||
|
|
state.query = "";
|
|||
|
|
state.category = "all";
|
|||
|
|
state.page = 1;
|
|||
|
|
elements.searchInput.value = "";
|
|||
|
|
elements.categorySelect.value = "all";
|
|||
|
|
clearChipActive();
|
|||
|
|
render();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function bindEvents() {
|
|||
|
|
elements.searchInput.addEventListener("input", (event) => {
|
|||
|
|
state.query = event.target.value;
|
|||
|
|
state.page = 1;
|
|||
|
|
if (!state.query.trim()) {
|
|||
|
|
clearChipActive();
|
|||
|
|
}
|
|||
|
|
render();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
elements.categorySelect.addEventListener("change", (event) => {
|
|||
|
|
state.category = event.target.value;
|
|||
|
|
state.page = 1;
|
|||
|
|
render();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
elements.sortSelect.addEventListener("change", (event) => {
|
|||
|
|
state.sortBy = event.target.value;
|
|||
|
|
state.page = 1;
|
|||
|
|
render();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
elements.resetBtn.addEventListener("click", () => {
|
|||
|
|
resetFilters();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
elements.hotKeywords.addEventListener("click", (event) => {
|
|||
|
|
const chip = event.target.closest(".chip");
|
|||
|
|
if (!chip) {
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
clearChipActive();
|
|||
|
|
chip.classList.add("active");
|
|||
|
|
state.query = chip.dataset.keyword || "";
|
|||
|
|
state.page = 1;
|
|||
|
|
elements.searchInput.value = state.query;
|
|||
|
|
render();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
if (elements.categorySidebarList) {
|
|||
|
|
elements.categorySidebarList.addEventListener("click", (event) => {
|
|||
|
|
const categoryButton = event.target.closest(".category-side-btn");
|
|||
|
|
if (!categoryButton) {
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
state.category = categoryButton.dataset.category || "all";
|
|||
|
|
state.page = 1;
|
|||
|
|
elements.categorySelect.value = state.category;
|
|||
|
|
render();
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
elements.prevBtn.addEventListener("click", () => {
|
|||
|
|
if (state.page > 1) {
|
|||
|
|
state.page -= 1;
|
|||
|
|
render();
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
elements.nextBtn.addEventListener("click", () => {
|
|||
|
|
const totalPages = Math.max(1, Math.ceil(filterTools().length / state.pageSize));
|
|||
|
|
if (state.page < totalPages) {
|
|||
|
|
state.page += 1;
|
|||
|
|
render();
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
elements.toolGrid.addEventListener("click", (event) => {
|
|||
|
|
const detailBtn = event.target.closest(".js-detail");
|
|||
|
|
if (detailBtn) {
|
|||
|
|
const tool = tools.find((item) => item.id === detailBtn.dataset.id);
|
|||
|
|
if (tool) {
|
|||
|
|
openModal(tool);
|
|||
|
|
}
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const downloadBtn = event.target.closest(".js-download");
|
|||
|
|
if (downloadBtn) {
|
|||
|
|
const tool = tools.find((item) => item.id === downloadBtn.dataset.id);
|
|||
|
|
if (tool) {
|
|||
|
|
downloadTool(tool);
|
|||
|
|
}
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const openBtn = event.target.closest(".js-open");
|
|||
|
|
if (openBtn) {
|
|||
|
|
const tool = tools.find((item) => item.id === openBtn.dataset.id);
|
|||
|
|
if (tool) {
|
|||
|
|
openWebTool(tool);
|
|||
|
|
}
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const clearBtn = event.target.closest("#clearEmptyBtn");
|
|||
|
|
if (clearBtn) {
|
|||
|
|
resetFilters();
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
elements.closeModalBtn.addEventListener("click", closeModal);
|
|||
|
|
elements.detailModal.addEventListener("click", (event) => {
|
|||
|
|
if (event.target === elements.detailModal) {
|
|||
|
|
closeModal();
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
elements.overviewBtn.addEventListener("click", openOverviewModal);
|
|||
|
|
elements.closeOverviewModalBtn.addEventListener("click", closeOverviewModal);
|
|||
|
|
elements.overviewModal.addEventListener("click", (event) => {
|
|||
|
|
if (event.target === elements.overviewModal) {
|
|||
|
|
closeOverviewModal();
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
document.addEventListener("keydown", (event) => {
|
|||
|
|
if (event.key === "Escape") {
|
|||
|
|
if (elements.detailModal.classList.contains("open")) {
|
|||
|
|
closeModal();
|
|||
|
|
}
|
|||
|
|
if (elements.overviewModal.classList.contains("open")) {
|
|||
|
|
closeOverviewModal();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
window.addEventListener("scroll", updateHeaderScrollState, {passive: true});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function init() {
|
|||
|
|
buildOptions();
|
|||
|
|
bindEvents();
|
|||
|
|
updateHeaderScrollState();
|
|||
|
|
render();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
init();
|