Update content and logic

This commit is contained in:
2026-02-12 19:39:45 +04:00
parent 9dc0790581
commit b7518fe85f
17 changed files with 206 additions and 35 deletions

View File

@@ -40,12 +40,19 @@ collections:
- { label: "Image", name: "image", widget: "image", required: false }
- { label: "Category", name: "category", widget: "select", options: ["actual","history","other"] }
- { label: "Tech Stack", name: "techStack", widget: "list", default: [] }
- { label: "Status", name: "status", widget: "select", options: ["completed","in-progress","planned","paused"] }
- label: "Status"
name: "status"
widget: "select"
options: ["completed","in-progress","planned","paused"]
- { label: "Live Demo", name: "liveDemo", widget: "string", required: false }
- { label: "Source Code", name: "sourceCode", widget: "string", required: false }
- { label: "Start Date", name: "startDate", widget: "datetime" }
- { label: "End Date", name: "endDate", widget: "datetime", required: false }
- { label: "Featured", name: "featured", widget: "boolean", required: false, default: false }
- label: "Featured"
name: "featured"
widget: "boolean"
required: false
default: false
- { label: "Tags", name: "tags", widget: "list", required: false, default: [] }
- name: "skills"

View File

@@ -133,16 +133,31 @@ const sizeClasses = getSizeClasses(size);
</p>
<!-- 技术栈 -->
{project.techStack && project.techStack.length > 0 && (
<div class="flex flex-wrap gap-1 mb-4">
<div class="flex flex-wrap gap-1 mb-4" data-tech-stack-container data-max-visible={maxTechStack}>
{project.techStack.slice(0, maxTechStack).map((tech) => (
<span class={`px-2 py-1 bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-300 rounded-sm ${sizeClasses.tech}`}>
{tech}
</span>
))}
{project.techStack.length > maxTechStack && (
<span class={`px-2 py-1 bg-gray-100 text-gray-700 dark:bg-gray-900/30 dark:text-gray-400 rounded-sm ${sizeClasses.tech}`}>
+{project.techStack.length - maxTechStack}
</span>
<>
{project.techStack.slice(maxTechStack).map((tech) => (
<span
class={`hidden px-2 py-1 bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-300 rounded-sm ${sizeClasses.tech} tech-stack-hidden`}
data-tech-hidden
>
{tech}
</span>
))}
<button
type="button"
class={`px-2 py-1 bg-gray-100 text-gray-700 dark:bg-gray-900/30 dark:text-gray-400 rounded-sm cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-800 transition-colors ${sizeClasses.tech}`}
data-tech-stack-toggle
data-hidden-count={project.techStack.length - maxTechStack}
>
+{project.techStack.length - maxTechStack}
</button>
</>
)}
</div>
)}
@@ -181,3 +196,38 @@ const sizeClasses = getSizeClasses(size);
</div>
</div>
</div>
<script>
// Раскрытие списка технологий при клике на "+"
document.addEventListener('DOMContentLoaded', () => {
const toggleButtons = document.querySelectorAll('[data-tech-stack-toggle]');
toggleButtons.forEach(button => {
button.addEventListener('click', (e) => {
const container = button.closest('[data-tech-stack-container]');
const hiddenElements = container?.querySelectorAll('[data-tech-hidden]');
const hiddenCount = parseInt(button.getAttribute('data-hidden-count') || '0');
if (hiddenElements && hiddenElements.length > 0) {
const isExpanded = !hiddenElements[0].classList.contains('hidden');
if (isExpanded) {
// Сворачиваем: скрываем все скрытые элементы
hiddenElements.forEach(el => {
el.classList.add('hidden');
});
button.textContent = `+${hiddenCount}`;
button.setAttribute('aria-expanded', 'false');
} else {
// Раскрываем: показываем все скрытые элементы
hiddenElements.forEach(el => {
el.classList.remove('hidden');
});
button.textContent = '';
button.setAttribute('aria-expanded', 'true');
}
}
});
});
});
</script>

View File

@@ -254,12 +254,32 @@ const itemColor = item.color || "#3B82F6";
<!-- 技能 -->
{item.skills && item.skills.length > 0 && (
<div class="mb-4">
<div class="flex flex-wrap gap-1">
{item.skills.map((skill) => (
<div class="flex flex-wrap gap-1" data-skills-container>
{item.skills.slice(0, 3).map((skill) => (
<span class={`px-2 py-1 bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-300 rounded-sm ${sizeClasses.badge}`}>
{skill}
</span>
))}
{item.skills.length > 3 && (
<>
{item.skills.slice(3).map((skill) => (
<span
class={`hidden px-2 py-1 bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-300 rounded-sm ${sizeClasses.badge} skills-hidden`}
data-skill-hidden
>
{skill}
</span>
))}
<button
type="button"
class={`px-2 py-1 bg-gray-100 text-gray-700 dark:bg-gray-900/30 dark:text-gray-400 rounded-sm cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-800 transition-colors ${sizeClasses.badge}`}
data-skills-toggle
data-hidden-count={item.skills.length - 3}
>
+{item.skills.length - 3}
</button>
</>
)}
</div>
</div>
)}
@@ -353,16 +373,31 @@ const itemColor = item.color || "#3B82F6";
<!-- 技能和链接 -->
<div class="flex items-center justify-between">
{item.skills && item.skills.length > 0 && (
<div class="flex flex-wrap gap-1">
<div class="flex flex-wrap gap-1" data-skills-container>
{item.skills.slice(0, 3).map((skill) => (
<span class={`px-2 py-1 bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-300 rounded-sm ${sizeClasses.badge}`}>
{skill}
</span>
))}
{item.skills.length > 3 && (
<span class={`px-2 py-1 bg-gray-100 text-gray-700 dark:bg-gray-900/30 dark:text-gray-400 rounded-sm ${sizeClasses.badge}`}>
+{item.skills.length - 3}
</span>
<>
{item.skills.slice(3).map((skill) => (
<span
class={`hidden px-2 py-1 bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-300 rounded-sm ${sizeClasses.badge} skills-hidden`}
data-skill-hidden
>
{skill}
</span>
))}
<button
type="button"
class={`px-2 py-1 bg-gray-100 text-gray-700 dark:bg-gray-900/30 dark:text-gray-400 rounded-sm cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-800 transition-colors ${sizeClasses.badge}`}
data-skills-toggle
data-hidden-count={item.skills.length - 3}
>
+{item.skills.length - 3}
</button>
</>
)}
</div>
)}
@@ -383,4 +418,41 @@ const itemColor = item.color || "#3B82F6";
</div>
</div>
</div>
)}
)}
<script>
// Раскрытие списка навыков при клике на "+"
// Используем делегирование событий для надёжности
document.addEventListener('click', (e) => {
const button = (e.target as HTMLElement).closest('[data-skills-toggle]') as HTMLElement;
if (!button) return;
e.preventDefault();
e.stopPropagation();
const container = button.closest('[data-skills-container]');
if (!container) return;
const hiddenElements = container.querySelectorAll('[data-skill-hidden]');
if (hiddenElements.length === 0) return;
const hiddenCount = parseInt(button.getAttribute('data-hidden-count') || '0');
const isExpanded = !hiddenElements[0].classList.contains('hidden');
if (isExpanded) {
// Сворачиваем: скрываем все скрытые элементы
hiddenElements.forEach(el => {
el.classList.add('hidden');
});
button.textContent = `+${hiddenCount}`;
button.setAttribute('aria-expanded', 'false');
} else {
// Раскрываем: показываем все скрытые элементы
hiddenElements.forEach(el => {
el.classList.remove('hidden');
});
button.textContent = '';
button.setAttribute('aria-expanded', 'true');
}
});
</script>

View File

@@ -3,7 +3,6 @@ title: NextCloud на домашнем сервере с офисным паке
published: 2026-02-12T19:26:00.000+04:00
cover: /images/nextcloud.png
tags:
- DevOps
- Linux
draft: false
---

View File

@@ -1,6 +1,6 @@
{
"sourceCode": "https://git.stepanovplaton.ru/StepanovPlaton/HomeServerServices",
"featured": true,
"featured": false,
"liveDemo": "",
"startDate": "2025-12-10T08:00:00.000+04:00",
"techStack": [

View File

@@ -1,6 +1,6 @@
{
"sourceCode": "https://git.stepanovplaton.ru/StepanovPlaton/jelly_belly_wiki",
"featured": false,
"featured": true,
"liveDemo": "https://jelly-belly-wiki.vercel.app",
"startDate": "2024-07-08T08:00:00.000+04:00",
"techStack": [

View File

@@ -1,6 +1,6 @@
{
"sourceCode": "https://git.stepanovplaton.ru/StepanovPlaton/NeuralNetwork",
"featured": false,
"featured": true,
"startDate": "2025-10-26T08:00:00.000+04:00",
"techStack": [
"C++",

View File

@@ -1,6 +1,6 @@
{
"sourceCode": "https://git.stepanovplaton.ru/StepanovPlaton/OSin1000Lines",
"featured": true,
"featured": false,
"startDate": "2026-02-03T08:00:00.000+04:00",
"techStack": [
"C"

View File

@@ -1,35 +1,35 @@
# Это интересно!
Раз уж у меня появился свой уголок в интернете, я посчитал нужным поделиться с вами классными проектами других людей. Здесь вы найдёте и другие сайты, и книги, и видеоигры, от популярных до нишевых — **всё, что я хочу показать миру**.
Раз уж у меня появился свой уголок в интернете, решил поделиться с тобой классными проектами других людей. Здесь ты найдёшь и другие сайты, и книги, и видеоигры, от популярных до нишевых — **всё, что я хочу показать миру**. Надеюсь, тебе будет интересно!
## О дварфах!
История моей аватарки (стилистики этого сайта, моего сервера и много чего ещё) берёт своё начало с **[легендарной игры DwarfFortress](http://www.bay12games.com/dwarves/)**. Эта игра для меня пример **гениальности, усидчивости и абсурда** в одном лице.
<details>
<summary>Я не могу советовать её вам (потому что это игра не для всех), но был обязан упомянуть на своём сайте</summary>
<summary>Я не могу советовать её тебе (потому что это игра не для всех), но был обязан упомянуть на своём сайте</summary>
Её движок настолько сложен, что долгое время к нему не могли добавить графику. За **более чем 20 лет разработки** она готова менее чем наполовину, её делают два брата на пожертвования от фанатов, и при всём этом ей вдохновлялись создатели Minecraft и Rimworld, а в 2013 Нью-Йоркский музей современного искусства включил игру в свою коллекцию.
Это лучший симулятор историй. Обязательно почитайте [историю крепости Боутмёрдед](https://dtf.ru/games/22946-legendy-dwarf-fortress-saga-o-padenii-kreposti-boutmerded) — эти легенды тянут на эпичный фильм от Netflix, но это просто обычная игра в DwarfFortress. Да, здесь можно тренировать дварфов бегать на костылях быстрее, чем на ногах, стреляя в них монетками. Да, на вас может напасть огромная адская автоматически сгенерированная... попа. И да, ваши дварфы могут умереть не только от гоблинов, но и от кошек, безумия, миазмов, одинаковой выпивки и даже сушняка...
Это лучший симулятор историй. Обязательно почитай [историю крепости Боутмёрдед](https://dtf.ru/games/22946-legendy-dwarf-fortress-saga-o-padenii-kreposti-boutmerded) — эти легенды тянут на эпичный фильм от Netflix, но это просто обычная игра в DwarfFortress. Да, здесь можно тренировать дварфов бегать на костылях быстрее, чем на ногах, стреляя в них монетками. Да, на тебя может напасть огромная адская автоматически сгенерированная... попа. И да, твои дварфы могут умереть не только от гоблинов, но и от кошек, безумия, миазмов, одинаковой выпивки и даже сушняка...
Эта игра не для всех, но я боюсь в неё заходить, потому что потом не могу выйти.
И помните... Проигрывать весело!
И помни... Проигрывать весело!
</details>
## Карандаш и Самоделкин
У всякой истории есть начало. Я начал свою историю отсюда: [Блог команды "Карандаш и Самоделкин"](https://karandashsamodelkin.blogspot.com). Передавайте привет маленькому мне и **большое спасибо моему отцу**!
У всякой истории есть начало. Я начал свою историю отсюда: [Блог команды "Карандаш и Самоделкин"](https://karandashsamodelkin.blogspot.com). Передай привет маленькому мне и **огромное спасибо моему отцу**!
## Классные сайты
Интернет огромен, и в нём куча страниц. Некоторые популярны, другие не очень, но эти особенно интересны:
Интернет огромен, и в нём куча страниц. Некоторые популярны, другие не очень, но эти особенно зацепили меня:
+ [Онлайн книга Linux From Scratch](https://linuxfromscratch.org) — бесплатное руководство по созданию своей GNU/Linux системы из исходного кода (с нуля). Лучшее развлечение на вечер для админа.
+ [Этаж 796](https://floor796.com) — проект русского художника, который объединил на 796 этаже космической станции всех самых знаковых персонажей из мемов, фильмов, комиксов и сериалов. Тут залип на 2 часа...
+ [SCP Foundation](https://scpfoundation.net) — открытая научно-фантастическая онлайн вселенная. Мурашки по коже, невозможно оторваться. Обязательно прочтите [SCP-079 — Старый ИИ](https://scpfoundation.net/scp-079) и [Хаб отдела антимеметики](https://scpfoundation.net/antimemetics-division-hub).
+ [CashGo](https://cashgo.ru) — онлайн игра, тренажёр финансового интеллекта. Здесь я успел ухватить кусочек старого интернета с ламповыми форумами. Спасибо за детство, передавайте привет Оксюше, Пингвинатко, Успеху и Лису!
+ [SCP Foundation](https://scpfoundation.net) — открытая научно-фантастическая онлайн вселенная. Мурашки по коже, невозможно оторваться. Обязательно прочитай [SCP-079 — Старый ИИ](https://scpfoundation.net/scp-079) и [Хаб отдела антимеметики](https://scpfoundation.net/antimemetics-division-hub).
+ [CashGo](https://cashgo.ru) — онлайн игра, тренажёр финансового интеллекта. Здесь я успел ухватить кусочек старого интернета с ламповыми форумами. Спасибо за детство, передай привет Оксюше, Пингвинатко, Успеху и Лису!
+ [Неолурк](https://neolurk.org) — народная википедия.
## IT
@@ -38,7 +38,7 @@
+ [KISS](https://ru.wikipedia.org/wiki/KISS_(принцип)) — делай проще, тупица.
+ [Быстрый обратный квадратный корень](https://ru.wikipedia.org/wiki/Быстрый_обратный_квадратный_корень) — магическая функция приближённого вычисления $1/\sqrt{x}$ из Quake.
+ [В††](https://neolurk.org/wiki/В%2B%2B) — язык программирования русских богатырей.
+ [Suckless](https://suckless.org) — эталон минимализма. Обязательно зацените [dwm](https://dwm.suckless.org) и [st](https://st.suckless.org).
+ [Suckless](https://suckless.org) — эталон минимализма. Обязательно зацени [dwm](https://dwm.suckless.org) и [st](https://st.suckless.org).
+ [9600 бод и все-все-все](https://lib.ru/ANEKDOTY/9600.txt) — Винни Пух стал хакером времён FIDONET.
+ [Nand2Tetris](https://www.nand2tetris.org) — собираем 16-битный компьютер из логических блоков, пишем компилятор, операционную систему и видеоигры. Курс от MIT.
@@ -63,7 +63,7 @@
## Книги
Я не очень люблю читать книги, но просто обожаю слушать их аудиоверсии. Это книги которые кажутся мне классными. Я специально не стал писать их в список, иначе просто не смог бы определить порядок. Некоторые из них полезные, некоторые развлекательные, некоторые известные, другие нет. Может быть ты найдёшь здесь что-то для себя!
Я не очень люблю читать книги, но просто обожаю слушать их аудиоверсии. Это книги, которые кажутся мне классными. Я специально не стал писать их в список по порядку, иначе просто не смог бы определить, какая лучше. Некоторые из них полезные, некоторые развлекательные, некоторые известные, другие нет. Может быть, ты найдёшь здесь что-то для себя!
| | | |
|---|---|---|
@@ -94,7 +94,7 @@
## Музыка
+ [ГРОТ](https://grotmusic.ru) - не просто музыка, а текст и глубокий смысл.
+ [HTP](https://vk.com/nii_rap) - если вы из IT просто полистайте, это весело.
+ [HTP](https://vk.com/nii_rap) - если ты из IT просто полистай, это весело.
## Кумиры
@@ -106,6 +106,6 @@
## Разное
Это просто классные вещи, зацените их!
Это просто классные вещи, зацени их!
+ [Цикада 3301](https://habr.com/ru/companies/ruvds/articles/714806/) — одна из самых больших загадок интернета 2010-х.

View File

@@ -87,6 +87,7 @@ enum I18nKey {
projectsInProgress = "projectsInProgress",
projectsTechStack = "projectsTechStack",
projectsFeatured = "projectsFeatured",
projectsInWork = "projectsInWork",
projectsPlanned = "projectsPlanned",
projectsPaused = "projectsPaused",
projectsDemo = "projectsDemo",

View File

@@ -90,7 +90,8 @@ export const en: Translation = {
[Key.projectsCompleted]: "Completed",
[Key.projectsInProgress]: "In Progress",
[Key.projectsTechStack]: "Tech Stack Statistics",
[Key.projectsFeatured]: "Featured Projects",
[Key.projectsFeatured]: "Must See Projects",
[Key.projectsInWork]: "Currently Working On",
[Key.projectsPlanned]: "Planned",
[Key.projectsPaused]: "Paused",
[Key.projectsDemo]: "Live Demo",

View File

@@ -90,7 +90,8 @@ export const ja: Translation = {
[Key.projectsCompleted]: "完了",
[Key.projectsInProgress]: "進行中",
[Key.projectsTechStack]: "技術スタック統計",
[Key.projectsFeatured]: "注目プロジェクト",
[Key.projectsFeatured]: "必見プロジェクト",
[Key.projectsInWork]: "現在開発中",
[Key.projectsPlanned]: "予定",
[Key.projectsPaused]: "一時停止",
[Key.projectsDemo]: "ライブデモ",

View File

@@ -89,7 +89,8 @@ export const ru: Translation = {
[Key.projectsCompleted]: "Завершён",
[Key.projectsInProgress]: "В разработке",
[Key.projectsTechStack]: "Статистика технологий",
[Key.projectsFeatured]: "Сейчас я работаю над этим",
[Key.projectsFeatured]: "Обязательно посмотрите эти проекты",
[Key.projectsInWork]: "Сейчас я работаю над этим",
[Key.projectsPlanned]: "Запланированных",
[Key.projectsPaused]: "Приостановлен",
[Key.projectsDemo]: "Демо",

View File

@@ -90,7 +90,8 @@ export const zh: Translation = {
[Key.projectsCompleted]: "已完成",
[Key.projectsInProgress]: "进行中",
[Key.projectsTechStack]: "技术栈统计",
[Key.projectsFeatured]: "精选项目",
[Key.projectsFeatured]: "必看项目",
[Key.projectsInWork]: "正在开发中",
[Key.projectsPlanned]: "计划中",
[Key.projectsPaused]: "已暂停",
[Key.projectsDemo]: "在线演示",

View File

@@ -9,6 +9,7 @@ import {
getProjectStats,
getProjectsByCategory,
getFeaturedProjects,
getInWorkProjects,
getAllTechStack,
} from "@utils/projects";
import { UNCATEGORIZED } from "@constants/constants";
@@ -25,6 +26,7 @@ const subtitle = LinkPresets[LinkPreset.Projects].description;
// 获取项目统计信息
const stats = getProjectStats();
const featuredProjects = getFeaturedProjects();
const inWorkProjects = getInWorkProjects();
const allTechStack = getAllTechStack();
// 定义分类顺序
@@ -105,6 +107,22 @@ const getCategoryText = (category: string) => {
</div>
</div>
)}
<!-- In-work проекты -->
{inWorkProjects.length > 0 && (
<div class="mb-8">
<h2 class="text-2xl font-bold text-black/90 dark:text-white/90 mb-4">
{i18n(I18nKey.projectsInWork)}
<span class="text-lg font-normal text-black/60 dark:text-white/60 ml-2">
({inWorkProjects.length})
</span>
</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
{inWorkProjects.map((project) => (
<ProjectCard project={project} size="large" showImage={true} maxTechStack={4} />
))}
</div>
</div>
)}
<!-- 按分类展示项目 -->
<div class="space-y-8">
{categories.map((category) => {

View File

@@ -3,6 +3,7 @@ import { type CollectionEntry, getCollection } from "astro:content";
import { getCategoryUrl } from "@utils/url";
import { i18n } from "@i18n/translation";
import I18nKey from "@i18n/i18nKey";
import { UNCATEGORIZED } from "@constants/constants";
// // Retrieve posts and sort them by publication date
@@ -110,7 +111,12 @@ export async function getCategoryList(): Promise<Category[]> {
});
const ret: Category[] = [];
const uncategorizedKey = i18n(I18nKey.uncategorized);
for (const c of lst) {
// Исключаем категорию "Без категории" из списка
if (c === uncategorizedKey) {
continue;
}
ret.push({
name: c,
count: count[c],

View File

@@ -58,6 +58,9 @@ export const getProjectsByCategory = (category?: string) => {
} else {
filteredProjects = projectsData.filter((p) => p.category === category);
}
// Exclude featured projects and in-progress projects from category lists
// (they are shown in their dedicated sections)
filteredProjects = filteredProjects.filter((p) => !p.featured && p.status !== "in-progress");
// Sort by startDate in descending order (newest first)
return filteredProjects.sort((a, b) => {
const dateA = new Date(a.startDate).getTime();
@@ -68,7 +71,18 @@ export const getProjectsByCategory = (category?: string) => {
// Get featured projects
export const getFeaturedProjects = () => {
return projectsData.filter((p) => p.featured);
return projectsData.filter((p) => p.featured === true);
};
// Get in-work projects (in-progress status, excluding featured)
export const getInWorkProjects = () => {
return projectsData
.filter((p) => p.status === "in-progress" && !p.featured)
.sort((a, b) => {
const dateA = new Date(a.startDate).getTime();
const dateB = new Date(b.startDate).getTime();
return dateB - dateA;
});
};
// Get all tech stacks