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)
254 lines
10 KiB
JavaScript
254 lines
10 KiB
JavaScript
import React from 'react';
|
||
import Icon from './Icon';
|
||
|
||
/**
|
||
* Лента истории публикации проекта (синий wow-стиль Рублокса).
|
||
* Показывает события: публикация → блокировка → возврат и т.п.
|
||
*
|
||
* Premoderation убрана (RUBLOX_SMART_FEED_PLAN.md). Новые типы событий:
|
||
* published, submitted_review, blocked, unblocked, restored_feed.
|
||
* Старые типы (submitted, approved_rank*, needs_changes, rejected)
|
||
* оставлены — они есть в истории игр, опубликованных до перехода.
|
||
*
|
||
* Props:
|
||
* history — массив событий [{ event_type, actor_user_id, comment,
|
||
* age_rating, created_at, ... }]
|
||
*/
|
||
|
||
// Самописные inline-SVG-иконки вместо эмодзи (правило проекта).
|
||
const EV_ICONS = {
|
||
upload: <path d="M12 17V4M6 10l6-6 6 6M5 20h14" />,
|
||
check: <path d="M5 13l4 4L19 7" />,
|
||
search: <><circle cx="11" cy="11" r="6" /><path d="M20 20l-4.3-4.3" /></>,
|
||
pencil: <path d="M4 20l4-1 11-11-3-3L5 16zM14 5l3 3" />,
|
||
cross: <path d="M6 6l12 12M18 6L6 18" />,
|
||
lock: <><rect x="5" y="11" width="14" height="9" rx="2" /><path d="M8 11V8a4 4 0 018 0v3" /></>,
|
||
block: <><circle cx="12" cy="12" r="9" /><path d="M6 6l12 12" /></>,
|
||
};
|
||
|
||
const EVENT_META = {
|
||
// --- Новые события умной ленты ---
|
||
published: {
|
||
ico: 'check',
|
||
label: 'Опубликовано в ленте',
|
||
color: '#10b981',
|
||
bg: 'linear-gradient(135deg, #10b981 0%, #0f9d56 100%)',
|
||
glow: 'rgba(16, 185, 129, 0.35)',
|
||
},
|
||
submitted_review: {
|
||
ico: 'search',
|
||
label: 'Отправлено на быструю проверку',
|
||
color: '#f59e0b',
|
||
bg: 'linear-gradient(135deg, #f59e0b 0%, #d97706 100%)',
|
||
glow: 'rgba(245, 158, 11, 0.35)',
|
||
},
|
||
blocked: {
|
||
ico: 'block',
|
||
label: 'Заблокировано администрацией',
|
||
color: '#ef4444',
|
||
bg: 'linear-gradient(135deg, #ff6f7a 0%, #c0303f 100%)',
|
||
glow: 'rgba(239, 68, 68, 0.35)',
|
||
},
|
||
unblocked: {
|
||
ico: 'check',
|
||
label: 'Разблокировано',
|
||
color: '#10b981',
|
||
bg: 'linear-gradient(135deg, #10b981 0%, #0f9d56 100%)',
|
||
glow: 'rgba(16, 185, 129, 0.35)',
|
||
},
|
||
restored_feed: {
|
||
ico: 'check',
|
||
label: 'Возвращено в ленту',
|
||
color: '#10b981',
|
||
bg: 'linear-gradient(135deg, #10b981 0%, #0f9d56 100%)',
|
||
glow: 'rgba(16, 185, 129, 0.35)',
|
||
},
|
||
// --- Старые события (до перехода на умную ленту) ---
|
||
submitted: {
|
||
ico: 'upload',
|
||
label: 'Отправлено на модерацию',
|
||
color: '#f59e0b',
|
||
bg: 'linear-gradient(135deg, #f59e0b 0%, #d97706 100%)',
|
||
glow: 'rgba(245, 158, 11, 0.35)',
|
||
},
|
||
approved_rank1: {
|
||
ico: 'check',
|
||
label: 'Одобрено (главная лента)',
|
||
color: '#ef4444',
|
||
bg: 'linear-gradient(135deg, #ec4899 0%, #ef4444 50%, #f59e0b 100%)',
|
||
glow: 'rgba(239, 68, 68, 0.40)',
|
||
},
|
||
approved_rank2: {
|
||
ico: 'check',
|
||
label: 'Одобрено (только профиль)',
|
||
color: '#10b981',
|
||
bg: 'linear-gradient(135deg, #10b981 0%, #0f9d56 100%)',
|
||
glow: 'rgba(16, 185, 129, 0.35)',
|
||
},
|
||
needs_changes: {
|
||
ico: 'pencil',
|
||
label: 'Возвращено на доработку',
|
||
color: '#f97316',
|
||
bg: 'linear-gradient(135deg, #f97316 0%, #ea580c 100%)',
|
||
glow: 'rgba(249, 115, 22, 0.35)',
|
||
},
|
||
rejected: {
|
||
ico: 'cross',
|
||
label: 'Отклонено',
|
||
color: '#ef4444',
|
||
bg: 'linear-gradient(135deg, #ff6f7a 0%, #c0303f 100%)',
|
||
glow: 'rgba(239, 68, 68, 0.35)',
|
||
},
|
||
unpublished: {
|
||
ico: 'lock',
|
||
label: 'Снято с публикации',
|
||
color: '#94a3b8',
|
||
bg: 'linear-gradient(135deg, #94a3b8 0%, #64748b 100%)',
|
||
glow: 'rgba(100, 116, 139, 0.30)',
|
||
},
|
||
};
|
||
|
||
const ModerationHistory = ({ history, compact = false }) => {
|
||
if (!history || history.length === 0) {
|
||
return (
|
||
<div style={{
|
||
fontSize: 13, color: '#9a9a9e', fontStyle: 'italic',
|
||
padding: 14, textAlign: 'center',
|
||
background: '#1b1b1b',
|
||
border: '1px dashed #3a3a3a',
|
||
borderRadius: 12,
|
||
fontFamily: '"Roboto Condensed", system-ui, sans-serif',
|
||
}}>
|
||
История публикации пуста.
|
||
</div>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<div style={{
|
||
background: '#1b1b1b',
|
||
border: '1px solid #3a3a3a',
|
||
borderRadius: 14,
|
||
padding: compact ? 12 : 16,
|
||
maxHeight: compact ? 220 : 420,
|
||
overflowY: 'auto',
|
||
fontFamily: '"Roboto Condensed", system-ui, sans-serif',
|
||
}}>
|
||
<div style={{
|
||
fontSize: 11, fontWeight: 800, color: '#6d8aff',
|
||
marginBottom: 12, letterSpacing: 0.8, textTransform: 'uppercase',
|
||
display: 'inline-flex', alignItems: 'center', gap: 6,
|
||
}}>
|
||
<span style={{ fontSize: 14 }}><Icon name="script" size={14} /></span>
|
||
История публикации ({history.length})
|
||
</div>
|
||
|
||
<div style={{ position: 'relative' }}>
|
||
{/* Вертикальная линия */}
|
||
<div style={{
|
||
position: 'absolute',
|
||
left: 15, top: 8, bottom: 8,
|
||
width: 2,
|
||
background: 'linear-gradient(180deg, #4f74ff 0%, #3a3a3a 100%)',
|
||
borderRadius: 1,
|
||
}} />
|
||
{history.map((ev, i) => {
|
||
const meta = EVENT_META[ev.event_type] || EVENT_META.submitted;
|
||
return (
|
||
<div key={ev.id || i} style={{
|
||
position: 'relative',
|
||
paddingLeft: 42,
|
||
paddingBottom: 14,
|
||
paddingTop: 2,
|
||
}}>
|
||
{/* Кружок с иконкой */}
|
||
<div style={{
|
||
position: 'absolute',
|
||
left: 0, top: 0,
|
||
width: 32, height: 32,
|
||
borderRadius: '50%',
|
||
background: meta.bg,
|
||
border: '3px solid #1b1b1b',
|
||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||
color: '#fff',
|
||
fontWeight: 800,
|
||
zIndex: 1,
|
||
boxShadow: `0 4px 12px ${meta.glow}`,
|
||
}}>
|
||
<svg width={15} height={15} viewBox="0 0 24 24"
|
||
fill="none" stroke="currentColor" strokeWidth="2.4"
|
||
strokeLinecap="round" strokeLinejoin="round">
|
||
{EV_ICONS[meta.ico] || EV_ICONS.upload}
|
||
</svg>
|
||
</div>
|
||
|
||
{/* Заголовок события */}
|
||
<div style={{
|
||
fontSize: 13,
|
||
fontWeight: 800,
|
||
color: '#e8e8ea',
|
||
letterSpacing: -0.1,
|
||
}}>
|
||
{meta.label}
|
||
</div>
|
||
|
||
{/* Метаданные */}
|
||
<div style={{
|
||
fontSize: 11,
|
||
color: '#9a9a9e',
|
||
marginTop: 3,
|
||
fontWeight: 600,
|
||
display: 'flex', flexWrap: 'wrap', gap: 6,
|
||
}}>
|
||
<span>{ev.created_at}</span>
|
||
{ev.age_rating && (
|
||
<span style={pillStyle(meta.color)}>{ev.age_rating}+</span>
|
||
)}
|
||
{ev.actor_user_id && (
|
||
<span style={pillStyle('#9a9a9e')}>
|
||
#{ev.actor_user_id}
|
||
</span>
|
||
)}
|
||
</div>
|
||
|
||
{/* Комментарий модератора */}
|
||
{ev.comment && (
|
||
<div style={{
|
||
marginTop: 8,
|
||
padding: '10px 14px',
|
||
background: '#2e2e2e',
|
||
border: '1px solid #3a3a3a',
|
||
borderLeft: `3px solid ${meta.color}`,
|
||
borderRadius: '4px 10px 10px 4px',
|
||
fontSize: 13,
|
||
color: '#e8e8ea',
|
||
whiteSpace: 'pre-wrap',
|
||
wordBreak: 'break-word',
|
||
lineHeight: 1.5,
|
||
fontWeight: 500,
|
||
boxShadow: '0 1px 3px rgba(0, 0, 0, 0.3)',
|
||
}}>
|
||
{ev.comment}
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
const pillStyle = (color) => ({
|
||
display: 'inline-flex', alignItems: 'center',
|
||
background: `${color}22`,
|
||
border: `1px solid ${color}55`,
|
||
color: color,
|
||
padding: '1px 8px',
|
||
borderRadius: 999,
|
||
fontSize: 10, fontWeight: 800,
|
||
letterSpacing: 0.2,
|
||
});
|
||
|
||
export default ModerationHistory;
|