hello-page/script.js

260 lines
8.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const STORAGE_KEY = "bratskih-hub-preferences";
const serviceCards = [
{
title: "Nextcloud",
kicker: "Cloud storage",
description:
"Личное облако для файлов, заметок, календарей и синхронизации между устройствами.",
url: "https://nextcloud.bratskih.tech",
image: "assets/nextcloud.svg",
},
{
title: "Mail",
kicker: "Mailbox",
description:
"Почтовый шлюз для рабочей переписки, уведомлений и управления сообщениями.",
url: "https://mail.bratskih.tech",
image: "assets/mail.svg",
},
{
title: "Test Net",
kicker: "Lab network",
description:
"Песочница для экспериментов, тестовых окружений и проверки новых идей.",
url: "https://test-net.bratskih.tech",
image: "assets/test-net.svg",
},
{
title: "Gitea",
kicker: "Git forge",
description:
"Домашняя кузница кода: репозитории, задачи, ревью и история проектов.",
url: "https://gitea.bratskih.tech",
image: "assets/gitea.svg",
},
{
title: "BratskihTech openspace",
kicker: "Discussion room",
description:
"Комната для обсуждений, созвонов и быстрых встреч вокруг идей Bratskih Tech.",
url: "https://cloud.bratskih.tech/call/n49bjhdu",
image: "assets/openspace.svg",
},
{
title: "Обо мне",
kicker: "Profile",
description:
"Короткий раздел о владельце экосистемы, проектах и принципах домашней инфраструктуры.",
url: "#about",
image: "assets/about.svg",
target: "self",
variant: "about",
},
];
const defaultPreferences = {
theme: window.matchMedia("(prefers-color-scheme: light)").matches ? "light" : "dark",
view: "cards",
accent: "violet",
};
const preferences = {
...defaultPreferences,
...readPreferences(),
};
const services = document.querySelector("[data-cards-root]");
const settingsToggle = document.querySelector("[data-settings-toggle]");
const settingsPanel = document.querySelector("[data-settings-panel]");
const settingsOverlay = document.querySelector("[data-settings-overlay]");
const settingsClose = document.querySelector("[data-settings-close]");
const themeButtons = document.querySelectorAll("[data-theme]");
const viewButtons = document.querySelectorAll("[data-view]");
const accentButtons = document.querySelectorAll("[data-accent]");
renderCards(serviceCards);
applyPreferences(preferences);
attachCardInteractions();
settingsToggle.addEventListener("click", () => {
const isOpen = settingsPanel.classList.toggle("is-open");
settingsOverlay.classList.toggle("is-open", isOpen);
settingsToggle.setAttribute("aria-expanded", String(isOpen));
});
settingsClose.addEventListener("click", closeSettingsPanel);
settingsOverlay.addEventListener("click", closeSettingsPanel);
document.addEventListener("keydown", (event) => {
if (event.key === "Escape") {
closeSettingsPanel();
}
});
document.addEventListener("click", (event) => {
if (!settingsPanel.classList.contains("is-open")) {
return;
}
if (settingsPanel.contains(event.target) || settingsToggle.contains(event.target)) {
return;
}
closeSettingsPanel();
});
themeButtons.forEach((button) => {
button.addEventListener("click", () => {
preferences.theme = button.dataset.theme;
applyPreferences(preferences);
savePreferences(preferences);
});
});
viewButtons.forEach((button) => {
button.addEventListener("click", () => {
preferences.view = button.dataset.view;
applyPreferences(preferences);
savePreferences(preferences);
});
});
accentButtons.forEach((button) => {
button.addEventListener("click", () => {
preferences.accent = button.dataset.accent;
applyPreferences(preferences);
savePreferences(preferences);
});
});
function applyPreferences(nextPreferences) {
document.documentElement.dataset.theme = nextPreferences.theme;
document.documentElement.dataset.accent = nextPreferences.accent;
services.classList.toggle("cards-view", nextPreferences.view === "cards");
services.classList.toggle("list-view", nextPreferences.view === "list");
setActiveButton(themeButtons, "theme", nextPreferences.theme);
setActiveButton(viewButtons, "view", nextPreferences.view);
setActiveButton(accentButtons, "accent", nextPreferences.accent);
}
function setActiveButton(buttons, key, value) {
buttons.forEach((button) => {
const isActive = button.dataset[key] === value;
button.classList.toggle("is-active", isActive);
button.setAttribute("aria-pressed", String(isActive));
});
}
function readPreferences() {
try {
return JSON.parse(localStorage.getItem(STORAGE_KEY)) || {};
} catch {
return {};
}
}
function savePreferences(nextPreferences) {
localStorage.setItem(STORAGE_KEY, JSON.stringify(nextPreferences));
}
function closeSettingsPanel() {
settingsPanel.classList.remove("is-open");
settingsOverlay.classList.remove("is-open");
settingsToggle.setAttribute("aria-expanded", "false");
}
function clamp(value, min, max) {
return Math.min(Math.max(value, min), max);
}
function renderCards(cards) {
services.replaceChildren(...cards.map(createCardElement));
}
function createCardElement(card) {
const link = document.createElement("a");
link.className = "service-card";
link.href = card.url;
if (card.variant) {
link.dataset.variant = card.variant;
}
if (card.target !== "self") {
link.target = "_blank";
link.rel = "noopener noreferrer";
}
const shine = document.createElement("span");
shine.className = "shine";
const image = document.createElement("img");
image.src = card.image;
image.alt = "";
image.loading = "lazy";
const content = document.createElement("div");
const kicker = document.createElement("p");
kicker.className = "service-kicker";
kicker.textContent = card.kicker;
const title = document.createElement("h2");
title.textContent = card.title;
const description = document.createElement("p");
description.textContent = card.description;
const arrow = document.createElement("span");
arrow.className = "arrow";
arrow.setAttribute("aria-hidden", "true");
arrow.textContent = card.target === "self" ? "↓" : "→";
content.append(kicker, title, description);
link.append(shine, image, content, arrow);
return link;
}
function attachCardInteractions() {
document.querySelectorAll(".service-card").forEach((card) => {
let leaveTimer;
card.addEventListener("pointerenter", () => {
window.clearTimeout(leaveTimer);
card.classList.add("is-hovered");
});
card.addEventListener("pointermove", (event) => {
window.clearTimeout(leaveTimer);
card.classList.add("is-hovered");
const rect = card.getBoundingClientRect();
const x = event.clientX - rect.left;
const y = event.clientY - rect.top;
const xPercent = clamp((x / rect.width) * 100, 8, 92);
const yPercent = clamp((y / rect.height) * 100, 8, 92);
const rotateY = ((xPercent - 50) / 50) * 2.6;
const rotateX = ((50 - yPercent) / 50) * 2.6;
card.style.setProperty("--card-x", `${xPercent}%`);
card.style.setProperty("--card-y", `${yPercent}%`);
card.style.setProperty("--tilt-x", `${rotateX}deg`);
card.style.setProperty("--tilt-y", `${rotateY}deg`);
});
card.addEventListener("pointerleave", () => {
leaveTimer = window.setTimeout(() => {
card.classList.remove("is-hovered");
card.style.setProperty("--card-x", "50%");
card.style.setProperty("--card-y", "50%");
card.style.setProperty("--tilt-x", "0deg");
card.style.setProperty("--tilt-y", "0deg");
}, 80);
});
});
}