299 lines
13 KiB
JavaScript
Executable file
299 lines
13 KiB
JavaScript
Executable file
/**
|
||
* Matrix Cycling Blog - Data Store & Post Scheduler
|
||
* Tailored for MatrixHasYou // Ulyanovsk (Volga), Aspect Air 2025 29" MTB, Books, Gym, Tennis, Journal.
|
||
*/
|
||
|
||
window.MatrixStore = (function () {
|
||
const STORAGE_KEY_POSTS = "matrix_cycling_posts_v6";
|
||
const STORAGE_KEY_PHOTOS = "matrix_cycling_photos_v6";
|
||
|
||
const DEFAULT_NOTES = [
|
||
{
|
||
"id": "note-aspect-specs-001",
|
||
"type": "note",
|
||
"category": "howto",
|
||
"title": "🚴♂️ ТТХ моего велосипеда: Aspect Air 29\" (2025) — Полный мануал, стандарты и расходники",
|
||
"date": "2026-08-24T00:30:00.000Z",
|
||
"publishAt": "2026-08-24T00:30:00.000Z",
|
||
"status": "published",
|
||
"heroImage": "images/aspect_matrix_bridge.jpg",
|
||
"tags": [
|
||
"aspect_air_2025",
|
||
"29er",
|
||
"mtb_specs",
|
||
"shimano_cues",
|
||
"ulyanovsk",
|
||
"matrix"
|
||
],
|
||
"telemetry": {
|
||
"distance": "1x9 Shimano CUES",
|
||
"avgSpeed": "Sunshine 11-46T 9S",
|
||
"elevation": "Prowheel 34T (24mm)",
|
||
"watts": "EXSHO Air 100mm (34mm)"
|
||
},
|
||
"content": `Полная техническая спецификация, стандарты узлов, размеры расходников и геометрия найнера **Aspect Air 29" (2025)**. Этот мануал служит эталонным справочником по всем компонентам для обслуживания, ТО и быстрого подбора запчастей при ремонте.
|
||
|
||
---
|
||
|
||
### 1. ⚙️ Трансмиссия и Привод (Drivetrain)
|
||
* **Конфигурация:** \`1x9\` скоростей
|
||
* **Задний переключатель:** \`Shimano CUES RD-U4000\` (с технологией Shadow RD, прямая подводка троса)
|
||
* **Манетка переключения:** \`Shimano CUES SL-U4000\` (технология 2-Way Release)
|
||
* **Система шатунов:** \`PROWHEEL C10Y-NW 34T\` с полым валом **Ø24 мм** (Hollowtech II совместимый стандарт)
|
||
* Длина шатунов: **175 мм** (для рам M / L / XL) / **170 мм** (для рамы S)
|
||
* Звезда: **34 зуба (34T)**, профиль **Narrow-Wide** (защита от спадания цепи)
|
||
* **Каретка:** Резьбовая \`BSA 68/73 мм\` (B902 / внешние промышленные подшипники под вал 24 мм)
|
||
* **Кассета:** \`Sunshine MTB-CS-HR9-46 9S\` (диапазон **11-46T**: звезды 11-13-16-20-24-28-34-40-46)
|
||
* **Барабан втулки:** Стандарт \`Shimano HG (HyperGlide)\`
|
||
* **Цепь:** \`KMC XGLIDE\` (9 скоростей, замок быстросъемный)
|
||
|
||
---
|
||
|
||
### 2. 🛞 Колеса, Обода и Покрышки (Wheelset)
|
||
* **Размер колес:** \`29 дюймов (622 мм)\`
|
||
* **Обода:** \`Code D23race\` — двойные алюминиевые, **Tubeless Ready** (готовы под бескамерную установку), ETRTO \`622 x 23 мм\`, 32 спицы (32H), ниппель **Presta (FV)**
|
||
* **Передняя втулка:** \`Code H1.2\` (алюминиевый корпус на 2 промышленных подшипниках, стандарт **Boost 15x110 мм** на сквозной оси)
|
||
* **Задняя втулка:** \`Code H1.2\` (алюминиевый корпус на 4 промышленных подшипниках, стандарт **Boost 12x148 мм** на сквозной оси, барабан HG)
|
||
* **Покрышки:** \`Kenda Booster K1227 Skinwall 29x2.25"\` (универсальный накатистый XC-протектор со светлым бортом)
|
||
* **Рабочее давление шин:**
|
||
* Грунт / Корни / Песок: **1.8–1.9 бар (26–28 PSI)**
|
||
* Асфальт / Твердое покрытие: **2.0–2.2 бар (29–32 PSI)**
|
||
|
||
---
|
||
|
||
### 3. 🔱 Амортизационная вилка (Suspension Fork)
|
||
* **Модель:** \`EXSHO FIX 15 AIR / HLO / REB\`
|
||
* **Тип:** Воздушно-масляная (Air Spring)
|
||
* **Ход вилки:** **100 мм**
|
||
* **Ось:** Сквозная **Boost 15x110 мм**
|
||
* **Ноги:** Анодированный алюминиевый сплав, диаметр **Ø34 мм** (повышенная торсионная жесткость)
|
||
* **Бушинги:** Металло-тефлоновые скользящие направляющие
|
||
* **Регулировки:**
|
||
1. *Воздушная камера (Air Preload)* — накачка насосом высокого давления под вес райдера
|
||
2. *Блокировка (HLO - Hydraulic Lockout)* — флажок на короне вилки
|
||
3. *Регулировка отскока (Rebound)* — крутилка в нижней части правой ноги
|
||
|
||
---
|
||
|
||
### 4. 🛑 Тормозная система (Braking System)
|
||
* **Тормоза:** \`Shimano MT200\` — двухпоршневая гидравлика на минеральном масле Shimano Mineral Oil
|
||
* **Крепление заднего калипера:** \`Post Mount (PM)\` внутри заднего треугольника на нижнем пере (защита от ударов)
|
||
* **Передний ротор:** \`RPT-009 Ø180 мм\` (6 болтов)
|
||
* **Задний ротор:** \`RPT-009 Ø160 мм\` (6 болтов)
|
||
* **Тормозные колодки:** \`B01S / B03S / B05S Resin (органические)\`
|
||
|
||
---
|
||
|
||
### 5. 📐 Рама, Рулевое управление и Кокпит (Frame & Cockpit)
|
||
* **Материал рамы:** \`Alloy 6061\` (гидроформированные трубы переменного сечения, двойной баттинг, полированные швы *Smooth Welding*)
|
||
* **Стандарты рамы:** Дропауты под сквозную ось **Boost 12x148 мм**, скрытая внутренняя проводка тросов и гидролиний (*IHR / Silent Cables*)
|
||
* **Рулевая колонка:** \`Gineyea T05 52-52/39.8\` (полуинтегрированная на закрытых промподшипниках под конусный шток 1.5"-1.5")
|
||
* **Руль:** \`Code-802 Alloy\` — диаметр под вынос **Ø31.8 мм**, ширина **760 мм**, подъем 15 мм, загиб назад 6°
|
||
* **Вынос руля:** \`Code AS-002N Alloy\` (угол 0°, длина 60 мм под рамы M/L, 50 мм под S, 70 мм под XL)
|
||
* **Грипсы:** \`Code G104\` (эргономичный цепкий компаунд с алюминиевым замком Lock-on)
|
||
* **Подседельный штырь:** \`Code-609 Alloy\`, диаметр **Ø31.6 мм**, длина **400 мм**, надежный двухболтовый замок
|
||
* **Седло:** \`Code-12231\` (размеры **245 x 155 мм**, анатомический вырез и плотная спортивная пена)
|
||
* **Вес велосипеда:** ~**13.4 кг** (в сборе)
|
||
|
||
> [!TIP] Заметка для ТО и быстрого заказа расходников:
|
||
> * **Кассета:** 9 скоростей, барабан HG, диапазон 11-46T.
|
||
> * **Цепь:** 9-speed KMC / Shimano (116 звеньев).
|
||
> * **Колодки:** Shimano B05S Resin.
|
||
> * **Ниппель:** Presta (FV 48 мм).
|
||
> * **Оси:** Перед 15x110 Boost, Зад 12x148 Boost.`
|
||
}
|
||
];
|
||
|
||
const DEFAULT_PHOTOS = [];
|
||
|
||
// --- Local Storage Management ---
|
||
|
||
function getStoredNotes() {
|
||
try {
|
||
const raw = localStorage.getItem(STORAGE_KEY_POSTS);
|
||
if (raw === null) {
|
||
localStorage.setItem(STORAGE_KEY_POSTS, JSON.stringify(DEFAULT_NOTES));
|
||
return DEFAULT_NOTES;
|
||
}
|
||
return JSON.parse(raw);
|
||
} catch (e) {
|
||
console.error("Error reading notes from localStorage", e);
|
||
return DEFAULT_NOTES;
|
||
}
|
||
}
|
||
|
||
function saveStoredNotes(notes) {
|
||
try {
|
||
localStorage.setItem(STORAGE_KEY_POSTS, JSON.stringify(notes));
|
||
} catch (e) {
|
||
console.error("Error saving notes to localStorage", e);
|
||
}
|
||
}
|
||
|
||
function getStoredPhotos() {
|
||
try {
|
||
const raw = localStorage.getItem(STORAGE_KEY_PHOTOS);
|
||
if (raw === null) {
|
||
localStorage.setItem(STORAGE_KEY_PHOTOS, JSON.stringify(DEFAULT_PHOTOS));
|
||
return DEFAULT_PHOTOS;
|
||
}
|
||
return JSON.parse(raw);
|
||
} catch (e) {
|
||
console.error("Error reading photos from localStorage", e);
|
||
return DEFAULT_PHOTOS;
|
||
}
|
||
}
|
||
|
||
function saveStoredPhotos(photos) {
|
||
try {
|
||
localStorage.setItem(STORAGE_KEY_PHOTOS, JSON.stringify(photos));
|
||
} catch (e) {
|
||
console.error("Error saving photos to localStorage", e);
|
||
}
|
||
}
|
||
|
||
// --- Public Queries ---
|
||
|
||
function isItemLive(item) {
|
||
if (!item || item.status === "draft") return false;
|
||
const pubDate = new Date(item.publishAt || item.date);
|
||
return pubDate <= new Date();
|
||
}
|
||
|
||
function getPublishedNotes(category = null) {
|
||
const now = new Date();
|
||
let notes = getStoredNotes().filter(note => {
|
||
if (note.status === "draft") return false;
|
||
const pubDate = new Date(note.publishAt || note.date);
|
||
return pubDate <= now;
|
||
});
|
||
|
||
if (category && category !== "all") {
|
||
notes = notes.filter(n => n.category === category);
|
||
}
|
||
|
||
return notes.sort((a, b) => new Date(b.publishAt || b.date) - new Date(a.publishAt || a.date));
|
||
}
|
||
|
||
function getAllNotes() {
|
||
return getStoredNotes().sort((a, b) => new Date(b.date || b.publishAt) - new Date(a.date || a.publishAt));
|
||
}
|
||
|
||
function getPublishedPhotos() {
|
||
const now = new Date();
|
||
return getStoredPhotos().filter(photo => {
|
||
if (photo.status === "draft") return false;
|
||
const pubDate = new Date(photo.publishAt || photo.date);
|
||
return pubDate <= now;
|
||
}).sort((a, b) => new Date(b.publishAt || b.date) - new Date(a.publishAt || a.date));
|
||
}
|
||
|
||
function getAllPhotos() {
|
||
return getStoredPhotos().sort((a, b) => new Date(b.date || b.publishAt) - new Date(a.date || a.publishAt));
|
||
}
|
||
|
||
// --- Mutations ---
|
||
|
||
function saveNote(noteData) {
|
||
const notes = getStoredNotes();
|
||
const nowIso = new Date().toISOString();
|
||
|
||
if (noteData.id) {
|
||
const index = notes.findIndex(n => n.id === noteData.id);
|
||
if (index !== -1) {
|
||
notes[index] = { ...notes[index], ...noteData, updatedAt: nowIso };
|
||
} else {
|
||
notes.unshift({ ...noteData, createdAt: nowIso, updatedAt: nowIso });
|
||
}
|
||
} else {
|
||
const newId = "note-" + Date.now();
|
||
notes.unshift({
|
||
id: newId,
|
||
...noteData,
|
||
createdAt: nowIso,
|
||
updatedAt: nowIso
|
||
});
|
||
}
|
||
|
||
saveStoredNotes(notes);
|
||
return true;
|
||
}
|
||
|
||
function deleteNote(id) {
|
||
let notes = getStoredNotes();
|
||
notes = notes.filter(n => n.id !== id);
|
||
saveStoredNotes(notes);
|
||
return true;
|
||
}
|
||
|
||
function savePhoto(photoData) {
|
||
const photos = getStoredPhotos();
|
||
const nowIso = new Date().toISOString();
|
||
|
||
if (photoData.id) {
|
||
const index = photos.findIndex(p => p.id === photoData.id);
|
||
if (index !== -1) {
|
||
photos[index] = { ...photos[index], ...photoData, updatedAt: nowIso };
|
||
} else {
|
||
photos.unshift({ ...photoData, createdAt: nowIso, updatedAt: nowIso });
|
||
}
|
||
} else {
|
||
const newId = "photo-" + Date.now();
|
||
photos.unshift({
|
||
id: newId,
|
||
...photoData,
|
||
createdAt: nowIso,
|
||
updatedAt: nowIso
|
||
});
|
||
}
|
||
|
||
saveStoredPhotos(photos);
|
||
return true;
|
||
}
|
||
|
||
function deletePhoto(id) {
|
||
let photos = getStoredPhotos();
|
||
photos = photos.filter(p => p.id !== id);
|
||
saveStoredPhotos(photos);
|
||
return true;
|
||
}
|
||
|
||
function exportDatabase() {
|
||
return JSON.stringify({
|
||
version: "6.0",
|
||
exportedAt: new Date().toISOString(),
|
||
notes: getStoredNotes(),
|
||
photos: getStoredPhotos()
|
||
}, null, 2);
|
||
}
|
||
|
||
function importDatabase(jsonString) {
|
||
try {
|
||
const data = JSON.parse(jsonString);
|
||
if (Array.isArray(data.notes)) saveStoredNotes(data.notes);
|
||
if (Array.isArray(data.photos)) saveStoredPhotos(data.photos);
|
||
return true;
|
||
} catch (e) {
|
||
console.error("Import failed", e);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
return {
|
||
isItemLive,
|
||
getLiveNotes: getPublishedNotes,
|
||
getPublishedNotes,
|
||
getAllNotes,
|
||
getAllNotesForAdmin: getAllNotes,
|
||
getLivePhotos: getPublishedPhotos,
|
||
getPublishedPhotos,
|
||
getAllPhotos,
|
||
getAllPhotosForAdmin: getAllPhotos,
|
||
saveNote,
|
||
deleteNote,
|
||
savePhoto,
|
||
deletePhoto,
|
||
exportDatabase,
|
||
importDatabase
|
||
};
|
||
})();
|