테마: v0.2.8 및 상단 사용자 메뉴·검색·히어로·사이드바 누적 보정
Made-with: Cursor
This commit is contained in:
@@ -153,6 +153,30 @@
|
||||
|
||||
syncLeftSidebarState();
|
||||
|
||||
function initializeHomeHero() {
|
||||
document.querySelectorAll("[data-home-hero]").forEach(function (heroSection) {
|
||||
var heroImage = heroSection.querySelector("[data-home-hero-image]");
|
||||
|
||||
if (!heroImage) {
|
||||
return;
|
||||
}
|
||||
|
||||
function markHeroLoaded() {
|
||||
heroSection.classList.add("is-loaded");
|
||||
}
|
||||
|
||||
if (heroImage.complete && heroImage.naturalWidth > 0) {
|
||||
markHeroLoaded();
|
||||
return;
|
||||
}
|
||||
|
||||
heroImage.addEventListener("load", markHeroLoaded, { once: true });
|
||||
heroImage.addEventListener("error", markHeroLoaded, { once: true });
|
||||
});
|
||||
}
|
||||
|
||||
initializeHomeHero();
|
||||
|
||||
var tabRoot = document.querySelector("[data-tabs]");
|
||||
if (tabRoot) {
|
||||
var triggers = tabRoot.querySelectorAll("[data-tab-trigger]");
|
||||
@@ -176,43 +200,136 @@
|
||||
var searchModal = document.querySelector("[data-search-modal]");
|
||||
var searchInput = document.querySelector("[data-search-input]");
|
||||
var searchResults = document.querySelector("[data-search-results]");
|
||||
var searchResetButtons = document.querySelectorAll("[data-search-reset]");
|
||||
|
||||
function escapeSearchHtml(value) {
|
||||
return String(value || "")
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
|
||||
function getSearchItems() {
|
||||
var sourceItems = document.querySelectorAll("[data-search-source] [data-search-item]");
|
||||
var items = [];
|
||||
|
||||
document.querySelectorAll(".post-card h2 a, .recommended-list a, .category-chip, .author-list__item").forEach(function (link) {
|
||||
sourceItems.forEach(function (itemNode) {
|
||||
var title = (itemNode.dataset.searchTitle || "").trim();
|
||||
var url = (itemNode.dataset.searchUrl || "").trim();
|
||||
|
||||
if (!title || !url) {
|
||||
return;
|
||||
}
|
||||
|
||||
items.push({
|
||||
title: (link.textContent || "").trim(),
|
||||
url: link.getAttribute("href") || "#"
|
||||
type: itemNode.dataset.searchType || "post",
|
||||
title: title,
|
||||
url: url,
|
||||
excerpt: (itemNode.dataset.searchExcerpt || "").trim(),
|
||||
image: (itemNode.dataset.searchImage || "").trim()
|
||||
});
|
||||
});
|
||||
|
||||
return items.filter(function (item, index, array) {
|
||||
return item.title && array.findIndex(function (candidate) {
|
||||
return candidate.title === item.title && candidate.url === item.url;
|
||||
return array.findIndex(function (candidate) {
|
||||
return candidate.type === item.type && candidate.title === item.title && candidate.url === item.url;
|
||||
}) === index;
|
||||
});
|
||||
}
|
||||
|
||||
function renderSearchSection(heading, items, renderer) {
|
||||
if (!items.length) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return [
|
||||
'<section class="search-result-group">',
|
||||
'<h3 class="search-result-group__title">' + heading + "</h3>",
|
||||
'<div class="search-result-group__items">',
|
||||
items.map(renderer).join(""),
|
||||
"</div>",
|
||||
"</section>"
|
||||
].join("");
|
||||
}
|
||||
|
||||
function renderSearchResults(keyword) {
|
||||
var normalized = keyword.trim().toLowerCase();
|
||||
if (!normalized) {
|
||||
searchResults.innerHTML = '<p class="search-modal__hint">현재 페이지에 표시되는 게시물, 태그 및 작성자를 필터링하려면 입력을 시작하세요.</p>';
|
||||
searchResults.innerHTML = '<p class="search-modal__hint">검색어를 입력하면 Authors, Tags, Posts를 함께 보여줍니다.</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
var items = getSearchItems().filter(function (item) {
|
||||
return item.title.toLowerCase().indexOf(normalized) !== -1;
|
||||
var matchedItems = getSearchItems().filter(function (item) {
|
||||
var title = item.title.toLowerCase();
|
||||
var excerpt = item.excerpt.toLowerCase();
|
||||
return title.indexOf(normalized) !== -1 || excerpt.indexOf(normalized) !== -1;
|
||||
});
|
||||
|
||||
if (!items.length) {
|
||||
searchResults.innerHTML = '<p class="search-empty">현재 보기에 일치하는 항목이 없습니다.</p>';
|
||||
if (!matchedItems.length) {
|
||||
searchResults.innerHTML = '<p class="search-empty">일치하는 항목이 없습니다.</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
searchResults.innerHTML = items.map(function (item) {
|
||||
return '<a class="search-result" href="' + item.url + '">' + item.title + "</a>";
|
||||
}).join("");
|
||||
var authorItems = matchedItems.filter(function (item) {
|
||||
return item.type === "author";
|
||||
});
|
||||
var tagItems = matchedItems.filter(function (item) {
|
||||
return item.type === "tag";
|
||||
});
|
||||
var postItems = matchedItems.filter(function (item) {
|
||||
return item.type === "post";
|
||||
});
|
||||
|
||||
var resultMarkup = "";
|
||||
|
||||
resultMarkup += renderSearchSection("Authors", authorItems, function (item) {
|
||||
var imageMarkup = item.image
|
||||
? '<img class="search-author__avatar" src="' + escapeSearchHtml(item.image) + '" alt="' + escapeSearchHtml(item.title) + '">'
|
||||
: '<span class="search-author__avatar search-author__avatar--fallback">@</span>';
|
||||
|
||||
return [
|
||||
'<a class="search-result search-result--author" href="' + escapeSearchHtml(item.url) + '">',
|
||||
imageMarkup,
|
||||
'<span class="search-result__title">' + escapeSearchHtml(item.title) + "</span>",
|
||||
"</a>"
|
||||
].join("");
|
||||
});
|
||||
|
||||
resultMarkup += renderSearchSection("Tags", tagItems, function (item) {
|
||||
return [
|
||||
'<a class="search-result search-result--tag" href="' + escapeSearchHtml(item.url) + '">',
|
||||
'<span class="search-result__prefix">#</span>',
|
||||
'<span class="search-result__title">' + escapeSearchHtml(item.title) + "</span>",
|
||||
"</a>"
|
||||
].join("");
|
||||
});
|
||||
|
||||
resultMarkup += renderSearchSection("Posts", postItems, function (item) {
|
||||
var excerptMarkup = item.excerpt
|
||||
? '<p class="search-result__excerpt">' + escapeSearchHtml(item.excerpt) + "</p>"
|
||||
: "";
|
||||
|
||||
return [
|
||||
'<a class="search-result search-result--post" href="' + escapeSearchHtml(item.url) + '">',
|
||||
'<h4 class="search-result__title">' + escapeSearchHtml(item.title) + "</h4>",
|
||||
excerptMarkup,
|
||||
"</a>"
|
||||
].join("");
|
||||
});
|
||||
|
||||
searchResults.innerHTML = resultMarkup;
|
||||
}
|
||||
|
||||
function clearSearchInput() {
|
||||
if (!searchInput) {
|
||||
return;
|
||||
}
|
||||
|
||||
searchInput.value = "";
|
||||
renderSearchResults("");
|
||||
searchInput.focus();
|
||||
}
|
||||
|
||||
function toggleSearch(open) {
|
||||
@@ -242,6 +359,13 @@
|
||||
});
|
||||
});
|
||||
|
||||
searchResetButtons.forEach(function (button) {
|
||||
button.addEventListener("click", function (event) {
|
||||
event.preventDefault();
|
||||
clearSearchInput();
|
||||
});
|
||||
});
|
||||
|
||||
if (searchInput) {
|
||||
searchInput.addEventListener("input", function (event) {
|
||||
renderSearchResults(event.target.value);
|
||||
@@ -267,6 +391,153 @@
|
||||
var userThemeThumb = document.querySelector("[data-user-theme-thumb]");
|
||||
var userMenuTrack = document.querySelector("[data-user-menu-track]");
|
||||
var userMenuThumb = document.querySelector("[data-user-menu-thumb]");
|
||||
var memberNameDisplays = document.querySelectorAll("[data-member-name-display]");
|
||||
var memberAvatarImages = document.querySelectorAll("[data-member-avatar-image]");
|
||||
var memberAvatarBackgrounds = document.querySelectorAll("[data-member-avatar-background]");
|
||||
var memberAvatarInitials = document.querySelectorAll("[data-member-avatar-initial]");
|
||||
var memberPortalLinks = document.querySelectorAll("a[href^='#/portal/']");
|
||||
var memberRefreshInterval = null;
|
||||
|
||||
function getMemberDisplayName(member) {
|
||||
if (!member) {
|
||||
return "Anonymous";
|
||||
}
|
||||
|
||||
var name = (member.name || "").trim();
|
||||
return name || "Member";
|
||||
}
|
||||
|
||||
function getMemberInitial(member, seed) {
|
||||
var fallbackSeed = (seed || "").trim();
|
||||
var source = "";
|
||||
|
||||
if (member) {
|
||||
source = ((member.name || "").trim() || (member.email || "").trim());
|
||||
}
|
||||
|
||||
source = source || fallbackSeed || "M";
|
||||
return source.charAt(0).toUpperCase();
|
||||
}
|
||||
|
||||
function applyMemberUi(member) {
|
||||
if (!memberNameDisplays.length && !memberAvatarImages.length && !memberAvatarBackgrounds.length && !memberAvatarInitials.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
var displayName = getMemberDisplayName(member);
|
||||
var avatarImage = (member && member.avatar_image ? member.avatar_image : "").trim();
|
||||
var hasAvatarImage = !!avatarImage;
|
||||
|
||||
memberNameDisplays.forEach(function (element) {
|
||||
element.textContent = displayName;
|
||||
});
|
||||
|
||||
memberAvatarImages.forEach(function (element) {
|
||||
if (hasAvatarImage) {
|
||||
element.src = avatarImage;
|
||||
element.alt = displayName;
|
||||
element.classList.remove("hidden");
|
||||
|
||||
if (!element.dataset.errorBound) {
|
||||
element.addEventListener("error", function () {
|
||||
element.classList.add("hidden");
|
||||
});
|
||||
element.dataset.errorBound = "true";
|
||||
}
|
||||
} else {
|
||||
element.classList.add("hidden");
|
||||
}
|
||||
});
|
||||
|
||||
memberAvatarBackgrounds.forEach(function (element) {
|
||||
element.style.backgroundColor = "transparent";
|
||||
});
|
||||
|
||||
memberAvatarInitials.forEach(function (element) {
|
||||
var seed = element.dataset.memberAvatarSeed || "";
|
||||
element.textContent = getMemberInitial(member, seed);
|
||||
});
|
||||
}
|
||||
|
||||
function initializeMemberAvatarFromSeed() {
|
||||
memberAvatarInitials.forEach(function (element) {
|
||||
var seed = element.dataset.memberAvatarSeed || "";
|
||||
element.textContent = getMemberInitial(null, seed);
|
||||
});
|
||||
|
||||
memberAvatarBackgrounds.forEach(function (element) {
|
||||
element.style.backgroundColor = "transparent";
|
||||
});
|
||||
}
|
||||
|
||||
function getMemberFromPayload(payload) {
|
||||
if (!payload) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (payload.member) {
|
||||
return payload.member;
|
||||
}
|
||||
|
||||
if (Array.isArray(payload.members) && payload.members.length) {
|
||||
return payload.members[0];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function refreshMemberUi() {
|
||||
if (!memberNameDisplays.length && !memberAvatarImages.length && !memberAvatarBackgrounds.length && !memberAvatarInitials.length) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return window
|
||||
.fetch("/members/api/member/?_=" + Date.now(), {
|
||||
method: "GET",
|
||||
credentials: "include",
|
||||
cache: "no-store",
|
||||
headers: {
|
||||
Accept: "application/json"
|
||||
}
|
||||
})
|
||||
.then(function (response) {
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to fetch member");
|
||||
}
|
||||
|
||||
return response.json();
|
||||
})
|
||||
.then(function (payload) {
|
||||
var member = getMemberFromPayload(payload);
|
||||
|
||||
if (member) {
|
||||
applyMemberUi(member);
|
||||
}
|
||||
})
|
||||
.catch(function () {});
|
||||
}
|
||||
|
||||
function scheduleMemberUiRefreshLoop() {
|
||||
var attempts = 0;
|
||||
var maxAttempts = 20;
|
||||
|
||||
if (memberRefreshInterval) {
|
||||
window.clearInterval(memberRefreshInterval);
|
||||
memberRefreshInterval = null;
|
||||
}
|
||||
|
||||
refreshMemberUi();
|
||||
|
||||
memberRefreshInterval = window.setInterval(function () {
|
||||
attempts += 1;
|
||||
refreshMemberUi();
|
||||
|
||||
if (attempts >= maxAttempts) {
|
||||
window.clearInterval(memberRefreshInterval);
|
||||
memberRefreshInterval = null;
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
function syncUserMenuToggles() {
|
||||
var isDark = body.classList.contains("theme-dark");
|
||||
@@ -316,6 +587,7 @@
|
||||
event.stopPropagation();
|
||||
setUserMenu(userMenu.classList.contains("invisible"));
|
||||
syncUserMenuToggles();
|
||||
refreshMemberUi();
|
||||
});
|
||||
|
||||
document.addEventListener("click", function (event) {
|
||||
@@ -349,7 +621,25 @@
|
||||
});
|
||||
}
|
||||
|
||||
initializeMemberAvatarFromSeed();
|
||||
syncUserMenuToggles();
|
||||
refreshMemberUi();
|
||||
|
||||
memberPortalLinks.forEach(function (link) {
|
||||
link.addEventListener("click", function () {
|
||||
scheduleMemberUiRefreshLoop();
|
||||
});
|
||||
});
|
||||
|
||||
document.addEventListener("visibilitychange", function () {
|
||||
if (document.visibilityState === "visible") {
|
||||
refreshMemberUi();
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener("focus", function () {
|
||||
refreshMemberUi();
|
||||
});
|
||||
|
||||
var recommendationsPortalTrigger = document.querySelector("[data-portal='recommendations']");
|
||||
var recommendationsPortalTitle = recommendationsPortalTrigger ? recommendationsPortalTrigger.getAttribute("data-portal-title") : "";
|
||||
|
||||
Reference in New Issue
Block a user