Open-source веб-студия для создания игр Рублокса, двойная лицензия AGPL-3.0 + Коммерческая. Главное: - Vite 5 + React 18 + Babylon 7.54.3 + Monaco Editor + Colyseus 0.16 - Самодостаточный движок ~28к строк (66 файлов): BlockManager, TerrainVoxelBuilder, ModelManager, DecoManager, PlayerController, ScriptSandboxWorker, MultiplayerSync, 30+ GD-гейммодов - Главный редактор KubikonEditor (~37к строк) + панели, ScriptEditor (Monaco) - Витрина игр (KubikonFeed, KubikonStudio, KubikonDocs, KubikonLearn) - Geometry Dash sub-app (GdMenu, GdShop, GdRules, GdCoverArt) - 10 admin-preview каталогов для дизайнеров (скины, музыка, порталы и т.д.) - Конфигурируемый бэкенд через VITE_API_BASE — работает со staging (dev-api.rublox.pro) без настройки - Standalone-режим (VITE_STANDALONE=true) — открыть пустой редактор без бэка - Полная документация (на русском): README, ARCHITECTURE, CONTRIBUTING, SECURITY, CHANGELOG - ESLint + Prettier + EditorConfig - Legal: LICENSE (AGPL-3.0), LICENSE-COMMERCIAL.md, CLA.md, COPYRIGHT.md - Issue templates: bug_report, feature_request, security_disclosure Перед публикацией: - Все импорты из minecraftia заменены на локальные - Все хардкоды URL (minecraftia-school.ru) и внутренних IP убраны → env - Admin-эндпоинты Kubikon3DService вырезаны (остаются в приватном репо) - AdminKubikonModeration не публикуется (модерация — в team.rublox.pro) - 93 МБ ассетов public/kubikon-assets вынесены в .gitignore (раздаются через release artifact)
72 lines
2.8 KiB
JavaScript
72 lines
2.8 KiB
JavaScript
/**
|
||
* Утилиты форматирования времени для Рублокс 3D.
|
||
*
|
||
* Сервер сохраняет время в формате 'YYYY-MM-DD HH:MM:SS' БЕЗ маркера TZ.
|
||
* Это UTC (контейнер запущен в UTC), но строка ISO-парсера JS воспринимает
|
||
* её как local time → разница с реальным локальным.
|
||
*
|
||
* Эти функции трактуют сырое время как UTC и приводят к локальному.
|
||
*/
|
||
|
||
/**
|
||
* Парсит 'YYYY-MM-DD HH:MM:SS' (как UTC) или ISO ('...Z' / '+03:00') в Date.
|
||
*/
|
||
export function parseServerTime(s) {
|
||
if (!s) return null;
|
||
if (typeof s !== 'string') return null;
|
||
// Если уже ISO с TZ — Date справится
|
||
if (/Z$|[+-]\d{2}:?\d{2}$/.test(s)) {
|
||
const d = new Date(s);
|
||
return isNaN(d.getTime()) ? null : d;
|
||
}
|
||
// 'YYYY-MM-DD HH:MM:SS' — добавляем 'Z' чтобы интерпретировать как UTC
|
||
const iso = s.replace(' ', 'T') + 'Z';
|
||
const d = new Date(iso);
|
||
return isNaN(d.getTime()) ? null : d;
|
||
}
|
||
|
||
/** HH:MM в локальном поясе пользователя. */
|
||
export function formatTimeShort(s) {
|
||
const d = parseServerTime(s);
|
||
if (!d) return '';
|
||
return d.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' });
|
||
}
|
||
|
||
/** Полное «01.05.2026 11:33» в локальном поясе. */
|
||
export function formatDateTime(s) {
|
||
const d = parseServerTime(s);
|
||
if (!d) return '';
|
||
return d.toLocaleString(undefined, {
|
||
day: '2-digit', month: '2-digit', year: 'numeric',
|
||
hour: '2-digit', minute: '2-digit',
|
||
});
|
||
}
|
||
|
||
/** «5 минут назад» / «вчера» / «01.05.2026 11:33». */
|
||
export function formatRelative(s) {
|
||
const d = parseServerTime(s);
|
||
if (!d) return '';
|
||
const diffSec = (Date.now() - d.getTime()) / 1000;
|
||
if (diffSec < 0) return formatTimeShort(s); // в будущем — fallback
|
||
if (diffSec < 60) return 'только что';
|
||
if (diffSec < 3600) return `${Math.floor(diffSec / 60)} мин назад`;
|
||
if (diffSec < 86400) {
|
||
const h = Math.floor(diffSec / 3600);
|
||
return `${h} ${pluralRu(h, ['час', 'часа', 'часов'])} назад`;
|
||
}
|
||
if (diffSec < 86400 * 2) return 'вчера в ' + formatTimeShort(s);
|
||
if (diffSec < 86400 * 7) {
|
||
const days = Math.floor(diffSec / 86400);
|
||
return `${days} ${pluralRu(days, ['день', 'дня', 'дней'])} назад`;
|
||
}
|
||
return formatDateTime(s);
|
||
}
|
||
|
||
function pluralRu(n, forms) {
|
||
const mod10 = n % 10;
|
||
const mod100 = n % 100;
|
||
if (mod10 === 1 && mod100 !== 11) return forms[0];
|
||
if (mod10 >= 2 && mod10 <= 4 && (mod100 < 12 || mod100 > 14)) return forms[1];
|
||
return forms[2];
|
||
}
|