studio/src/community/KubikonStudio.jsx
МИН 30f0c622f1
Some checks failed
CI / Lint (pull_request) Failing after 40s
CI / Build (pull_request) Failing after 40s
CI / Secret scan (pull_request) Successful in 2m31s
CI / PR size check (pull_request) Successful in 6s
fix(studio): тёмный текст в PleeseReg + Профиль ведёт на rublox.pro
2026-05-28 15:02:05 +03:00

803 lines
42 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, { useState, useEffect } from 'react';
import { useNavigate, useSearchParams } from 'react-router-dom';
import { jwtDecode } from 'jwt-decode';
import { useAuth } from '../auth/AuthContext.jsx';
import * as Kubikon3DApi from '../api/Kubikon3DService';
import { USER_addres } from '../api/API';
import cl from './KubikonStudio.module.css';
import { TEMPLATES } from './templates';
import { ARTICLES as LEARN_ARTICLES } from './learnArticles';
import { generateAllTemplateScreenshots } from './templateScreenshots';
import PublishStatusBadge from '../editor/PublishStatusBadge';
import RublocsLogo from '../components/RublocsLogo/RublocsLogo';
import useDeviceType from '../hooks/useDeviceType';
import KubikonDesktopOnlyStub from './KubikonDesktopOnlyStub';
import PleeseReg from '../components/PleeseReg/PleeseReg';
import Icon from '../editor/Icon';
function getCurrentUserId() {
try {
const token = localStorage.getItem('Authorization');
if (!token) return null;
return jwtDecode(token).id;
} catch {
return null;
}
}
/** Жанры — для отображения в карточках. Должно совпадать с GENRES в GameSettingsModal. */
const GENRE_NAME = {
platformer: 'Платформер',
racing: 'Гонки',
shooter: 'Шутер',
sandbox: 'Песочница',
adventure: 'Приключение',
puzzle: 'Головоломка',
tycoon: 'Тайкун',
rpg: 'РПГ',
other: 'Другое',
};
/**
* Хелпер рендера карточки проекта (используется в нескольких секциях).
*/
function renderProjectCard(p, navigate, genreMap, onDeleteClick, cl, requireAuth = (a) => a()) {
const open = () => requireAuth(() => navigate(`/edit/${p.id}`));
return (
<div
key={p.id}
role="button"
tabIndex={0}
className={cl.templateCard}
onClick={open}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
open();
}
}}
title={p.title}
>
<div className={cl.templatePreview}>
{p.thumbnail ? (
<img src={p.thumbnail} alt={p.title} />
) : (
<div className={cl.templateEmoji}><Icon name="gamepad" size={14} /></div>
)}
{p.status && p.status !== 'draft' && (
<div style={{ position: 'absolute', top: 8, right: 8, zIndex: 5 }}>
<PublishStatusBadge status={p.status} comment={p.moderator_comment} />
</div>
)}
</div>
<div className={cl.templateBody}>
<div className={cl.templateTitle}>{p.title}</div>
<div className={cl.templateDesc}>
<span style={{ opacity: 0.92 }}>{genreMap[p.genre] || ''}</span>
{p.genre && <span style={{ margin: '0 6px', opacity: 0.4 }}>·</span>}
{p.updated_at?.slice(0, 10) || '—'}
<button
onClick={(e) => onDeleteClick(p.id, p.title, e)}
style={{
float: 'right', background: 'transparent', border: 'none',
color: 'var(--text-muted)', cursor: 'pointer', fontSize: 14, padding: 0
}}
title="Удалить"
><Icon name="delete" size={13} /> </button>
</div>
</div>
</div>
);
}
/**
* KubikonStudio — стартовый экран Studio для платформы Рублокс
* (наш аналог Roblox Studio с Minecraft-эстетикой).
*
* На этом экране (как в Roblox Studio Home):
* - крупная карточка «Создать новую игру»
* - сетка шаблонов (Платформер, Гонки, Шутер, Песочница, Пустой мир)
* - блок «Мои игры» (пока пустой)
* - блок «Последние»
* - блок «Обучение»
*
* Доступ: открыт всем авторизованным пользователям (раньше — только admin).
*/
const KubikonStudio = () => {
const navigate = useNavigate();
const { isAuthenticated, isLoading, user } = useAuth();
// Главный сайт Рублокса — туда уходит «Профиль» (свой реальный профиль
// у студии нет, профили живут на rublox.pro).
const RUBLOX_HOME =
(typeof import.meta !== 'undefined' && import.meta.env?.VITE_RUBLOX_HOME) ||
'https://rublox.pro/app';
const { isDesktop } = useDeviceType();
// Активная вкладка. Начальное значение можно задать через ?tab= в
// URL — так на Studio переходят из вики (у неё нет своих вкладок).
const [searchParams] = useSearchParams();
const VALID_TABS = ['home', 'recent', 'my', 'templates', 'archive'];
const initialTab = VALID_TABS.includes(searchParams.get('tab'))
? searchParams.get('tab') : 'home';
const [activeTab, setActiveTab] = useState(initialTab);
const [myProjects, setMyProjects] = useState([]);
const [myProjectsLoading, setMyProjectsLoading] = useState(true);
const [creatingTemplate, setCreatingTemplate] = useState(null);
// { id, title } | null — управляет модалкой подтверждения удаления
const [deleteTarget, setDeleteTarget] = useState(null);
const [deleting, setDeleting] = useState(false);
// Прогресс генерации скринов шаблонов
const [genProgress, setGenProgress] = useState(null); // null | {current, total, id}
// Имя пользователя для приветствия (как «Welcome, ...» в Roblox Studio)
const [greetName, setGreetName] = useState('');
// Поиск по своим играм. searchOpen — раскрыт ли инпут в шапке.
const [searchQuery, setSearchQuery] = useState('');
const [searchOpen, setSearchOpen] = useState(false);
// Гость МОЖЕТ просматривать студию — видит шаблоны и обучение.
// При попытке создать игру / открыть редактор / документацию — модалка.
const [showRegModal, setShowRegModal] = useState(false);
const requireAuth = (action) => {
if (isAuthenticated) {
action();
} else {
setShowRegModal(true);
}
};
// Грузим список «Мои игры»
useEffect(() => {
if (!isAuthenticated) return;
const userId = getCurrentUserId();
if (!userId) { setMyProjectsLoading(false); return; }
Kubikon3DApi.getMyProjects(userId)
.then(res => setMyProjects(res.data?.projects || []))
.catch(err => console.error('[KubikonStudio] my-projects error:', err))
.finally(() => setMyProjectsLoading(false));
}, [isAuthenticated]);
// Имя пользователя — для приветствия на главной. Берём из профиля
// (в JWT-токене имени нет, только id). firstName — основное поле,
// nikname — запасное.
useEffect(() => {
if (!isAuthenticated) { setGreetName(''); return; }
const jwt = localStorage.getItem('Authorization') || localStorage.getItem('player_jwt');
fetch(`${USER_addres}/api/v1/users/profile`, { headers: { Authorization: jwt || '' } })
.then(r => r.ok ? r.json() : null)
.then(d => {
const p = d?.data || d || {};
setGreetName(p.firstName || p.nikname || '');
})
.catch(err => console.error('[KubikonStudio] profile name error:', err));
}, [isAuthenticated]);
const handleDeleteProject = (projectId, projectTitle, e) => {
e.stopPropagation();
setDeleteTarget({ id: projectId, title: projectTitle || 'Без названия' });
};
const confirmDelete = async () => {
if (!deleteTarget || deleting) return;
const userId = getCurrentUserId();
if (!userId) return;
setDeleting(true);
try {
await Kubikon3DApi.deleteProject(deleteTarget.id, userId);
setMyProjects(prev => prev.filter(p => p.id !== deleteTarget.id));
setDeleteTarget(null);
} catch (err) {
console.error('[KubikonStudio] delete error:', err);
alert('Не удалось удалить проект');
} finally {
setDeleting(false);
}
};
if (isLoading) {
return null;
}
// «Изучай Студию» — карточки-статьи (контент в learnArticles.jsx,
// страница чтения — KubikonLearn.jsx). Берём первые 6 статей.
const GUIDES = LEARN_ARTICLES.slice(0, 6);
const handleTemplateClick = async (tpl) => {
if (!isAuthenticated) {
setShowRegModal(true);
return;
}
const userId = getCurrentUserId();
if (!userId || creatingTemplate) return;
setCreatingTemplate(tpl.id);
try {
const state = tpl.build();
const payload = {
user_id: userId,
title: tpl.title,
description: tpl.desc,
genre: tpl.genre || 'sandbox',
thumbnail: '',
is_public: false,
project_data: JSON.stringify(state),
};
const res = await Kubikon3DApi.createProject(userId, payload);
const newId = res.data?.id;
if (newId) {
navigate(`/edit/${newId}`);
} else {
throw new Error('No id from server');
}
} catch (err) {
console.error('[KubikonStudio] template create error:', err);
// Лимит игр на аккаунт — показываем понятное сообщение с сервера
const msg = err?.response?.data?.message;
alert(msg || 'Не удалось создать игру из шаблона');
setCreatingTemplate(null);
}
};
const handleGenerateScreenshots = async () => {
if (genProgress) return;
setGenProgress({ current: 0, total: TEMPLATES.length, id: '' });
try {
await generateAllTemplateScreenshots((p) => setGenProgress(p));
alert(
`Готово! ${TEMPLATES.length} превью скачаны.\n\n` +
`Скопируй файлы в:\npublic/assets/kubikon-templates/`
);
} catch (err) {
console.error('[KubikonStudio] generate screenshots error:', err);
alert('Не удалось сгенерировать превью');
} finally {
setGenProgress(null);
}
};
// Сортировка для секции «Последние» — по updated_at, top-6
const recentProjects = [...myProjects]
.sort((a, b) => (b.updated_at || '').localeCompare(a.updated_at || ''))
.slice(0, 6);
// Для вкладки «Мои игры»: опубликованные сверху, остальные снизу,
// внутри каждой группы — по updated_at desc.
// Умная лента: опубликованная игра — единый статус 'published'.
const isApproved = (p) => p.status === 'published';
const sortedMyProjects = [...myProjects].sort((a, b) => {
const pa = isApproved(a) ? 1 : 0;
const pb = isApproved(b) ? 1 : 0;
if (pa !== pb) return pb - pa; // одобренные первыми
return (b.updated_at || '').localeCompare(a.updated_at || '');
});
const publishedProjects = sortedMyProjects.filter(isApproved);
const draftProjects = sortedMyProjects.filter(p => !isApproved(p));
// 3D-редактор требует мышь и клавиатуру — на телефонах/планшетах
// показываем заглушку. Проверка ПОСЛЕ всех хуков, чтобы не нарушить
// правило rules-of-hooks (одинаковый порядок вызовов).
if (!isDesktop) {
return <KubikonDesktopOnlyStub feature="Рублокс" />;
}
return (
<div className={cl.studio}>
{/* === Модалка для гостя — войди или зарегистрируйся === */}
{showRegModal && (
<div
onClick={() => setShowRegModal(false)}
style={{
position: 'fixed', top: 0, left: 0, right: 0, bottom: 0,
background: 'rgba(15, 8, 35, 0.78)',
backdropFilter: 'blur(8px)',
WebkitBackdropFilter: 'blur(8px)',
zIndex: 9999,
display: 'flex', alignItems: 'center', justifyContent: 'center',
padding: 20,
}}
>
<div
onClick={(e) => e.stopPropagation()}
style={{
background: '#fff',
borderRadius: 20,
padding: 30,
position: 'relative',
width: '100%', maxWidth: 460,
boxShadow: '0 30px 80px rgba(0,0,0,0.5)',
}}
>
<button
onClick={() => setShowRegModal(false)}
style={{
position: 'absolute', top: 12, right: 12,
width: 36, height: 36, borderRadius: 18,
border: 'none',
background: '#F3E8FF', color: '#1E1B4B',
fontSize: 20, fontWeight: 800,
cursor: 'pointer',
}}
aria-label="Закрыть"
><Icon name="close" size={14} /></button>
<PleeseReg textDefault='Чтобы создавать игры в Рублоксе' style={{width:'auto',height:'auto'}} />
</div>
</div>
)}
{/* Левая боковая панель — навигация */}
<aside className={cl.sidebar}>
<div className={cl.sidebarHeader}>
<div className={cl.sidebarLogo}>
<RublocsLogo size={44} bg="#16181d" />
</div>
<div className={cl.sidebarTitle}>
<div className={cl.brandName}>Рублокс</div>
<div className={cl.brandSub}>Студия</div>
</div>
</div>
{/* Навигация в стиле Roblox Studio:
Новая игра · История · Главная · Мои игры · Шаблоны · Архив · ВИКИ */}
<nav className={cl.sidebarNav}>
{/* Новая игра — отдельная акцентная кнопка-действие */}
<button
className={cl.navNewGame}
onClick={() => requireAuth(() => navigate('/edit/new'))}
>
<span className={cl.navNewGameIco}><Icon name="plus" size={15} /></span>
<span>Новая игра</span>
</button>
<button
className={`${cl.navItem} ${activeTab === 'recent' ? cl.navActive : ''}`}
onClick={() => setActiveTab('recent')}
>
<span className={cl.navIcon}><Icon name="clock" size={15} /></span>
<span>История</span>
</button>
<button
className={`${cl.navItem} ${activeTab === 'home' ? cl.navActive : ''}`}
onClick={() => setActiveTab('home')}
>
<span className={cl.navIcon}><Icon name="home" size={15} /></span>
<span>Главная</span>
</button>
<button
className={`${cl.navItem} ${activeTab === 'my' ? cl.navActive : ''}`}
onClick={() => setActiveTab('my')}
>
<span className={cl.navIcon}><Icon name="folder" size={15} /></span>
<span>Мои игры</span>
</button>
<button
className={`${cl.navItem} ${activeTab === 'templates' ? cl.navActive : ''}`}
onClick={() => setActiveTab('templates')}
>
<span className={cl.navIcon}><Icon name="grid" size={15} /></span>
<span>Шаблоны</span>
</button>
<button
className={`${cl.navItem} ${activeTab === 'archive' ? cl.navActive : ''}`}
onClick={() => setActiveTab('archive')}
>
<span className={cl.navIcon}><Icon name="archive" size={15} /></span>
<span>Архив</span>
</button>
<button
className={cl.navItem}
onClick={() => navigate('/docs')}
title="Вики — учебник по редактору и 50 игр-уроков"
>
<span className={cl.navIcon}><Icon name="book-open" size={15} /></span>
<span>ВИКИ</span>
</button>
</nav>
<div className={cl.sidebarFooter}>
<button
className={cl.docsBtn}
onClick={() => navigate('/rules')}
style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 6 }}
>
<Icon name="shield" size={15} /> Правила создания игр
</button>
</div>
</aside>
{/* Основной контент */}
<main className={cl.main}>
<header className={cl.topBar}>
<div>
<h1 className={cl.pageTitle}>
{activeTab === 'home'
? (greetName ? `Привет, ${greetName}!` : 'Добро пожаловать в Студию')
: activeTab === 'recent' ? 'История'
: activeTab === 'my' ? 'Мои игры'
: activeTab === 'templates' ? 'Шаблоны'
: activeTab === 'archive' ? 'Архив'
: 'Студия'}
</h1>
<p className={cl.pageSub}>
{activeTab === 'home' ? 'Создавай 3D-игры и делись ими с друзьями'
: activeTab === 'recent' ? 'Игры, которые ты открывал последними'
: activeTab === 'my' ? 'Все твои игры — черновики и опубликованные'
: activeTab === 'templates' ? 'Готовые заготовки — начни игру в один клик'
: activeTab === 'archive' ? 'Сюда попадают игры, убранные в архив'
: ''}
</p>
</div>
<div className={cl.topBarActions}>
{/* Поиск по своим играм. Кнопка раскрывается в
инпут; непустой запрос показывает результаты. */}
{searchOpen ? (
<div
className={cl.searchBox}
style={{ display: 'inline-flex', alignItems: 'center', gap: 6, padding: '0 6px 0 14px' }}
>
<Icon name="search" size={14} />
<input
autoFocus
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="Поиск по моим играм…"
style={{
background: 'transparent', border: 'none', outline: 'none',
color: 'inherit', fontFamily: 'inherit', fontSize: 13,
width: 180,
}}
/>
<button
onClick={() => { setSearchQuery(''); setSearchOpen(false); }}
style={{
background: 'transparent', border: 'none', cursor: 'pointer',
color: 'inherit', fontSize: 16, padding: '6px 8px', lineHeight: 1,
}}
title="Закрыть поиск"
>×</button>
</div>
) : (
<button
className={cl.searchBox}
onClick={() => setSearchOpen(true)}
style={{ display: 'inline-flex', alignItems: 'center', gap: 6 }}
>
<Icon name="search" size={14} /> Поиск
</button>
)}
<button
className={cl.profileBtn}
onClick={() => {
// Профили живут на rublox.pro, не в студии.
// У гостя профиля нет — открываем главную сайта.
const url = (isAuthenticated && user?.id)
? `${RUBLOX_HOME}/profile/${user.id}`
: RUBLOX_HOME;
window.open(url, '_blank', 'noopener');
}}
style={{ display: 'inline-flex', alignItems: 'center', gap: 6 }}
>
<Icon name="user" size={14} /> Профиль
</button>
</div>
</header>
{/* ═══ РЕЗУЛЬТАТЫ ПОИСКА — когда в шапке введён запрос.
Ищем по своим играм (название/описание). Пока
поиск активен — обычные секции скрыты. ═══ */}
{searchQuery.trim() && (
<section className={cl.section}>
<div className={cl.sectionHead}>
<h3 className={cl.sectionTitle}>
Результаты поиска: «{searchQuery.trim()}»
</h3>
</div>
{(() => {
const q = searchQuery.trim().toLowerCase();
const found = myProjects.filter(p =>
(p.title || '').toLowerCase().includes(q)
|| (p.description || '').toLowerCase().includes(q));
if (myProjectsLoading) {
return <div className={cl.emptyStateSmall}><Icon name="hourglass" size={13} /> Загрузка...</div>;
}
if (found.length === 0) {
return (
<div className={cl.emptyState}>
<div className={cl.emptyEmoji}><Icon name="search" size={14} /></div>
<div className={cl.emptyTitle}>Ничего не найдено</div>
<div className={cl.emptyDesc}>
По запросу «{searchQuery.trim()}» среди твоих игр ничего нет
</div>
</div>
);
}
return (
<div className={cl.templatesGrid}>
{found.map(p =>
renderProjectCard(p, navigate, GENRE_NAME, handleDeleteProject, cl, requireAuth))}
</div>
);
})()}
</section>
)}
{/* ═══ HERO — крупный баннер с кадром игры (как Roblox Studio).
Кадр hero-226.png, текст слева, кнопка «Новая игра». ═══ */}
{!searchQuery.trim() && activeTab === 'home' && (
<section className={cl.hero}>
<img
className={cl.heroImg}
src="/wiki/hero/hero-226.png"
alt="Игра в Рублоксе"
/>
<div className={cl.heroOverlay} />
<div className={cl.heroContent}>
<div className={cl.heroBadge}>НАЧНИ ЗДЕСЬ</div>
<h2 className={cl.heroTitle}>Создавай свои миры</h2>
<p className={cl.heroDesc}>
Замки, подземелья, целые королевства. Редактор
Рублокса справится с любой задумкой начни прямо
сейчас.
</p>
<button
className={cl.heroBtn}
onClick={() => requireAuth(() => navigate('/edit/new'))}
>
<Icon name="plus" size={15} /> Новая игра
</button>
</div>
</section>
)}
{/* ═══ ШАБЛОНЫ — «Открой шаблон» (главная + вкладка Шаблоны) ═══ */}
{!searchQuery.trim() && (activeTab === 'home' || activeTab === 'templates') && (
<section className={cl.section}>
<div className={cl.sectionHead}>
<h3 className={cl.sectionTitle}>Открой шаблон</h3>
<span className={cl.sectionSub}>
Готовые заготовки популярных жанров или начни с нуля
</span>
</div>
<div className={cl.tplGrid}>
{TEMPLATES.map(t => (
<button
key={t.id}
className={cl.tplCard}
disabled={!!creatingTemplate}
onClick={() => handleTemplateClick(t)}
title={`Создать игру из шаблона «${t.title}»`}
style={{ '--tpl-color': t.color }}
>
<div className={cl.tplImageWrap}>
<img
src={`/assets/kubikon-templates/${t.id}.jpg`}
alt={t.title}
className={cl.tplImage}
onError={(e) => { e.currentTarget.style.display = 'none'; }}
/>
{/* Фолбэк-фон при отсутствии картинки */}
<div
className={cl.tplFallback}
style={{ background: `linear-gradient(135deg, ${t.color} 0%, #2a2218 100%)` }}
>
<span className={cl.tplFallbackEmoji}>
<Icon name={t.icon} size={40} />
</span>
</div>
<div className={cl.tplOverlay} />
<div className={cl.tplContent}>
<div className={cl.tplBadge}>
<Icon name={t.icon} size={20} />
</div>
<div className={cl.tplTitle}>{t.title}</div>
<div className={cl.tplDesc}>{t.desc}</div>
</div>
<div className={cl.tplCta}>
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 6 }}>
<Icon name="play" size={14} /> Создать игру
</span>
</div>
{creatingTemplate === t.id && (
<div className={cl.tplLoading}>
<div className={cl.tplLoadingSpinner} />
<span>Создаём...</span>
</div>
)}
</div>
</button>
))}
</div>
</section>
)}
{/* ═══ ИЗУЧАЙ СТУДИЮ — карточки-статьи (Discover Studio).
Контент в learnArticles.jsx, читалка — KubikonLearn. ═══ */}
{!searchQuery.trim() && activeTab === 'home' && (
<section className={cl.section}>
<div className={cl.sectionHead}>
<h3 className={cl.sectionTitle}>Изучай Студию</h3>
<button className={cl.sectionMore} onClick={() => navigate('/learn')}>
Все статьи
</button>
</div>
<div className={cl.guideGrid}>
{GUIDES.map(g => (
<button
key={g.id}
type="button"
className={cl.guideCard}
onClick={() => navigate('/learn/' + g.id)}
>
<div className={cl.guideImage}>
{/* запасной фон ПОД картинкой */}
<div
className={cl.guideFallback}
style={{ background: `linear-gradient(135deg, ${g.color} 0%, #15151c 120%)` }}
>
<Icon name="book-open" size={42} />
</div>
<img
className={cl.guideImg}
src={`/assets/kubikon-learn/${g.cover}`}
alt={g.title}
loading="lazy"
onError={(e) => { e.currentTarget.style.display = 'none'; }}
/>
</div>
<div className={cl.guideBody}>
<div className={cl.guideTitle}>{g.title}</div>
<div className={cl.guideDesc}>{g.summary}</div>
</div>
</button>
))}
</div>
</section>
)}
{/* ═══ ИСТОРИЯ — последние открытые игры ═══ */}
{!searchQuery.trim() && activeTab === 'recent' && (
<section className={cl.section}>
{!isAuthenticated ? (
<div className={cl.emptyState}>
<div className={cl.emptyEmoji}><Icon name="clock" size={14} /></div>
<div className={cl.emptyTitle}>Войди в аккаунт</div>
<div className={cl.emptyDesc}>История открытых игр доступна после входа</div>
<button className={cl.emptyBtn} onClick={() => setShowRegModal(true)}>
Войти
</button>
</div>
) : myProjectsLoading ? (
<div className={cl.emptyStateSmall}><Icon name="hourglass" size={13} /> Загрузка...</div>
) : recentProjects.length === 0 ? (
<div className={cl.emptyState}>
<div className={cl.emptyEmoji}><Icon name="clock" size={14} /></div>
<div className={cl.emptyTitle}>История пуста</div>
<div className={cl.emptyDesc}>Открой любую игру она появится здесь</div>
<button className={cl.emptyBtn} onClick={() => navigate('/edit/new')}>
+ Создать игру
</button>
</div>
) : (
<div className={cl.templatesGrid}>
{recentProjects.map(p =>
renderProjectCard(p, navigate, GENRE_NAME, handleDeleteProject, cl, requireAuth))}
</div>
)}
</section>
)}
{/* ═══ МОИ ИГРЫ — опубликованные + черновики ═══ */}
{!searchQuery.trim() && activeTab === 'my' && (
<>
{!isAuthenticated ? (
<section className={cl.section}>
<div className={cl.emptyState}>
<div className={cl.emptyEmoji}><Icon name="folder" size={14} /></div>
<div className={cl.emptyTitle}>Войди в аккаунт</div>
<div className={cl.emptyDesc}>Твои игры хранятся в аккаунте Рублокса</div>
<button className={cl.emptyBtn} onClick={() => setShowRegModal(true)}>
Войти
</button>
</div>
</section>
) : myProjectsLoading ? (
<section className={cl.section}>
<div className={cl.emptyStateSmall}><Icon name="hourglass" size={13} /> Загрузка...</div>
</section>
) : myProjects.length === 0 ? (
<section className={cl.section}>
<div className={cl.emptyState}>
<div className={cl.emptyEmoji}><Icon name="package" size={14} /></div>
<div className={cl.emptyTitle}>Пока пусто</div>
<div className={cl.emptyDesc}>Создай свою первую игру она появится здесь</div>
<button className={cl.emptyBtn} onClick={() => navigate('/edit/new')}>
+ Создать игру
</button>
</div>
</section>
) : (
<>
{publishedProjects.length > 0 && (
<section className={cl.section}>
<div className={cl.sectionHead}>
<h3 className={cl.sectionTitle}>
<Icon name="globe" size={13} /> Опубликованные ({publishedProjects.length})
</h3>
</div>
<div className={cl.templatesGrid}>
{publishedProjects.map(p =>
renderProjectCard(p, navigate, GENRE_NAME, handleDeleteProject, cl, requireAuth))}
</div>
</section>
)}
{draftProjects.length > 0 && (
<section className={cl.section}>
<div className={cl.sectionHead}>
<h3 className={cl.sectionTitle}>
<Icon name="rename" size={13} /> Черновики ({draftProjects.length})
</h3>
</div>
<div className={cl.templatesGrid}>
{draftProjects.map(p =>
renderProjectCard(p, navigate, GENRE_NAME, handleDeleteProject, cl, requireAuth))}
</div>
</section>
)}
</>
)}
</>
)}
{/* ═══ АРХИВ — пока пусто (заглушка) ═══ */}
{!searchQuery.trim() && activeTab === 'archive' && (
<section className={cl.section}>
<div className={cl.emptyState}>
<div className={cl.emptyEmoji}><Icon name="archive" size={14} /></div>
<div className={cl.emptyTitle}>Архив пуст</div>
<div className={cl.emptyDesc}>
Сюда можно убирать игры, которые сейчас не нужны,
но удалять их жалко. Скоро.
</div>
</div>
</section>
)}
{/* Подвал-блок */}
<section className={cl.footerBlock}>
<div className={cl.footerInfo}>
Рублокс Studio · Альфа-версия
</div>
</section>
</main>
{/* Модалка подтверждения удаления */}
{deleteTarget && (
<div className={cl.modalBackdrop} onClick={() => !deleting && setDeleteTarget(null)}>
<div className={cl.modalCard} onClick={(e) => e.stopPropagation()}>
<div className={cl.modalIcon}><Icon name="delete" size={14} /></div>
<div className={cl.modalTitle}>Удалить игру?</div>
<div className={cl.modalText}>
Игра <b>«{deleteTarget.title}»</b> будет удалена безвозвратно.
Восстановить её будет нельзя.
</div>
<div className={cl.modalActions}>
<button
className={cl.modalCancelBtn}
onClick={() => setDeleteTarget(null)}
disabled={deleting}
>
Отмена
</button>
<button
className={cl.modalDeleteBtn}
onClick={confirmDelete}
disabled={deleting}
>
{deleting ? <><Icon name="hourglass" size={13} /> Удаляем...</> : <><Icon name="delete" size={13} /> Удалить</>}
</button>
</div>
</div>
</div>
)}
</div>
);
};
export default KubikonStudio;