studio/src/editor/ModerationHistory.jsx
МИН 31adbf151b Initial public release: Студия Рублокса v1.0
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)
2026-05-27 23:41:10 +03:00

254 lines
10 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;