генерация карточек из объекта добавление разделов чата и обо мне
This commit is contained in:
parent
d86f7f1952
commit
f37edb6d24
14
assets/about.svg
Normal file
14
assets/about.svg
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<svg width="160" height="160" viewBox="0 0 160 160" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect width="160" height="160" rx="44" fill="url(#bg)"/>
|
||||||
|
<circle cx="80" cy="62" r="24" fill="white"/>
|
||||||
|
<path d="M42 124c6-28 20-42 38-42s32 14 38 42" fill="white"/>
|
||||||
|
<path d="M43 45c11-16 29-24 51-20" stroke="#FDE68A" stroke-width="8" stroke-linecap="round"/>
|
||||||
|
<path d="M111 37l9-12 9 12 14 4-9 11 1 15-15-6-15 6 1-15-9-11 14-4z" fill="#FDE68A"/>
|
||||||
|
<path d="M66 112h28" stroke="#0F172A" stroke-opacity=".32" stroke-width="8" stroke-linecap="round"/>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="bg" x1="18" y1="14" x2="140" y2="150" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="#22D3EE"/>
|
||||||
|
<stop offset="1" stop-color="#0F172A"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 781 B |
17
assets/openspace.svg
Normal file
17
assets/openspace.svg
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<svg width="160" height="160" viewBox="0 0 160 160" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect width="160" height="160" rx="44" fill="url(#bg)"/>
|
||||||
|
<circle cx="56" cy="67" r="18" fill="white"/>
|
||||||
|
<circle cx="104" cy="67" r="18" fill="white" fill-opacity=".86"/>
|
||||||
|
<path d="M32 112c4-20 17-32 35-32s31 12 35 32" fill="white"/>
|
||||||
|
<path d="M72 112c4-20 17-32 35-32 13 0 24 7 30 20" fill="white" fill-opacity=".76"/>
|
||||||
|
<path d="M39 44h82c8 0 14 6 14 14v30c0 8-6 14-14 14h-12l-17 18v-18H39c-8 0-14-6-14-14V58c0-8 6-14 14-14z" fill="#EEF2FF" fill-opacity=".2"/>
|
||||||
|
<path d="M52 67h55M52 84h33" stroke="white" stroke-width="8" stroke-linecap="round"/>
|
||||||
|
<circle cx="120" cy="42" r="14" fill="#FDE68A"/>
|
||||||
|
<path d="M114 42h12M120 36v12" stroke="#7C3AED" stroke-width="4" stroke-linecap="round"/>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="bg" x1="18" y1="14" x2="140" y2="150" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="#A78BFA"/>
|
||||||
|
<stop offset="1" stop-color="#4F46E5"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1023 B |
84
index.html
84
index.html
@ -82,82 +82,20 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="services cards-view" aria-label="Сервисы экосистемы">
|
<section
|
||||||
<a
|
class="services cards-view"
|
||||||
class="service-card"
|
aria-label="Сервисы экосистемы"
|
||||||
href="https://nextcloud.bratskih.tech"
|
data-cards-root
|
||||||
target="_blank"
|
></section>
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<span class="shine"></span>
|
|
||||||
<img src="assets/nextcloud.svg" alt="" loading="lazy" />
|
|
||||||
<div>
|
|
||||||
<p class="service-kicker">Cloud storage</p>
|
|
||||||
<h2>Nextcloud</h2>
|
|
||||||
<p>
|
|
||||||
Личное облако для файлов, заметок, календарей и синхронизации
|
|
||||||
между устройствами.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<span class="arrow" aria-hidden="true">→</span>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<a
|
<section class="about-panel" id="about" aria-labelledby="about-title">
|
||||||
class="service-card"
|
<p class="eyebrow">About</p>
|
||||||
href="https://mail.bratskih.tech"
|
<h2 id="about-title">Обо мне</h2>
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<span class="shine"></span>
|
|
||||||
<img src="assets/mail.svg" alt="" loading="lazy" />
|
|
||||||
<div>
|
|
||||||
<p class="service-kicker">Mailbox</p>
|
|
||||||
<h2>Mail</h2>
|
|
||||||
<p>
|
<p>
|
||||||
Почтовый шлюз для рабочей переписки, уведомлений и управления
|
Bratskih Tech — личное пространство для сервисов, экспериментов,
|
||||||
сообщениями.
|
инфраструктуры и проектов. Здесь собраны основные точки входа в мою
|
||||||
|
цифровую экосистему.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
|
||||||
<span class="arrow" aria-hidden="true">→</span>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<a
|
|
||||||
class="service-card"
|
|
||||||
href="https://test-net.bratskih.tech"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<span class="shine"></span>
|
|
||||||
<img src="assets/test-net.svg" alt="" loading="lazy" />
|
|
||||||
<div>
|
|
||||||
<p class="service-kicker">Lab network</p>
|
|
||||||
<h2>Test Net</h2>
|
|
||||||
<p>
|
|
||||||
Песочница для экспериментов, тестовых окружений и проверки новых
|
|
||||||
идей.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<span class="arrow" aria-hidden="true">→</span>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<a
|
|
||||||
class="service-card"
|
|
||||||
href="https://gitea.bratskih.tech"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<span class="shine"></span>
|
|
||||||
<img src="assets/gitea.svg" alt="" loading="lazy" />
|
|
||||||
<div>
|
|
||||||
<p class="service-kicker">Git forge</p>
|
|
||||||
<h2>Gitea</h2>
|
|
||||||
<p>
|
|
||||||
Домашняя кузница кода: репозитории, задачи, ревью и история
|
|
||||||
проектов.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<span class="arrow" aria-hidden="true">→</span>
|
|
||||||
</a>
|
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
|||||||
183
script.js
183
script.js
@ -1,5 +1,58 @@
|
|||||||
const STORAGE_KEY = "bratskih-hub-preferences";
|
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 = {
|
const defaultPreferences = {
|
||||||
theme: window.matchMedia("(prefers-color-scheme: light)").matches ? "light" : "dark",
|
theme: window.matchMedia("(prefers-color-scheme: light)").matches ? "light" : "dark",
|
||||||
view: "cards",
|
view: "cards",
|
||||||
@ -11,13 +64,14 @@ const preferences = {
|
|||||||
...readPreferences(),
|
...readPreferences(),
|
||||||
};
|
};
|
||||||
|
|
||||||
const services = document.querySelector(".services");
|
const services = document.querySelector("[data-cards-root]");
|
||||||
const themeButtons = document.querySelectorAll("[data-theme]");
|
const themeButtons = document.querySelectorAll("[data-theme]");
|
||||||
const viewButtons = document.querySelectorAll("[data-view]");
|
const viewButtons = document.querySelectorAll("[data-view]");
|
||||||
const accentButtons = document.querySelectorAll("[data-accent]");
|
const accentButtons = document.querySelectorAll("[data-accent]");
|
||||||
const cards = document.querySelectorAll(".service-card");
|
|
||||||
|
|
||||||
|
renderCards(serviceCards);
|
||||||
applyPreferences(preferences);
|
applyPreferences(preferences);
|
||||||
|
attachCardInteractions();
|
||||||
|
|
||||||
themeButtons.forEach((button) => {
|
themeButtons.forEach((button) => {
|
||||||
button.addEventListener("click", () => {
|
button.addEventListener("click", () => {
|
||||||
@ -43,43 +97,6 @@ accentButtons.forEach((button) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
cards.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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
function applyPreferences(nextPreferences) {
|
function applyPreferences(nextPreferences) {
|
||||||
document.documentElement.dataset.theme = nextPreferences.theme;
|
document.documentElement.dataset.theme = nextPreferences.theme;
|
||||||
document.documentElement.dataset.accent = nextPreferences.accent;
|
document.documentElement.dataset.accent = nextPreferences.accent;
|
||||||
@ -115,3 +132,91 @@ function savePreferences(nextPreferences) {
|
|||||||
function clamp(value, min, max) {
|
function clamp(value, min, max) {
|
||||||
return Math.min(Math.max(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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
37
styles.css
37
styles.css
@ -49,6 +49,10 @@ html[data-accent="rose"] {
|
|||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@ -257,7 +261,7 @@ h1 {
|
|||||||
|
|
||||||
.services {
|
.services {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
gap: 18px;
|
gap: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -306,6 +310,12 @@ h1 {
|
|||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.service-card[data-variant="about"] {
|
||||||
|
background:
|
||||||
|
linear-gradient(135deg, rgba(var(--accent-rgb), 0.2), transparent 58%),
|
||||||
|
var(--panel);
|
||||||
|
}
|
||||||
|
|
||||||
.service-card.is-hovered::before,
|
.service-card.is-hovered::before,
|
||||||
.service-card:focus-visible::before {
|
.service-card:focus-visible::before {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
@ -388,6 +398,31 @@ h1 {
|
|||||||
justify-self: end;
|
justify-self: end;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.about-panel {
|
||||||
|
margin-top: 18px;
|
||||||
|
padding: clamp(26px, 5vw, 44px);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 32px;
|
||||||
|
background: var(--panel);
|
||||||
|
box-shadow: var(--shadow);
|
||||||
|
backdrop-filter: blur(22px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.about-panel h2 {
|
||||||
|
margin: 10px 0 12px;
|
||||||
|
font-size: clamp(2rem, 5vw, 4.3rem);
|
||||||
|
line-height: 0.96;
|
||||||
|
letter-spacing: -0.07em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.about-panel p:not(.eyebrow) {
|
||||||
|
max-width: 780px;
|
||||||
|
margin: 0;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 1.05rem;
|
||||||
|
line-height: 1.75;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 1040px) {
|
@media (max-width: 1040px) {
|
||||||
.hero,
|
.hero,
|
||||||
.services {
|
.services {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user