/** * GdFinishesPreview — превью финишных ворот (10 эпох × 5 = 50). * Маршрут: /admin-preview/gd-finishes * Выбор юзера → kubikon3d_savegame (project_id=295, namespace='gd_finish_choices'). */ import React, { useEffect, useRef, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { useAuth } from '../auth/AuthContext.jsx'; import { jwtDecode } from 'jwt-decode'; import axios from 'axios'; import { STORYS_addres } from '../api/API'; import { Engine } from '@babylonjs/core/Engines/engine'; import { Scene } from '@babylonjs/core/scene'; import { ArcRotateCamera } from '@babylonjs/core/Cameras/arcRotateCamera'; import { HemisphericLight } from '@babylonjs/core/Lights/hemisphericLight'; import { DirectionalLight } from '@babylonjs/core/Lights/directionalLight'; import { Vector3, Color3, Color4 } from '@babylonjs/core/Maths/math'; import { MeshBuilder } from '@babylonjs/core/Meshes/meshBuilder'; import { StandardMaterial } from '@babylonjs/core/Materials/standardMaterial'; import { FINISH_CATALOG, EPOCH_INFO, getFinishesByEpoch } from './gdFinishes/finishFactories'; const CHOICES_PID = 295; const CHOICES_NS = 'gd_finish_choices'; function getUserId() { try { const t = localStorage.getItem('Authorization'); if (!t) return 0; return Number(jwtDecode(t).id) || 0; } catch (e) { return 0; } } const api = axios.create({ baseURL: STORYS_addres, timeout: 15000 }); api.interceptors.request.use((cfg) => { try { const token = localStorage.getItem('Authorization'); if (token) cfg.headers.Authorization = token; } catch (e) {} return cfg; }); function FinCard({ fin, isChosen, onChoose }) { const wrapRef = useRef(null); const canvasRef = useRef(null); const [isVisible, setIsVisible] = useState(false); useEffect(() => { if (!wrapRef.current) return; const io = new IntersectionObserver((entries) => { for (const e of entries) setIsVisible(e.isIntersecting); }, { rootMargin: '200px', threshold: 0.01 }); io.observe(wrapRef.current); return () => io.disconnect(); }, []); useEffect(() => { if (!isVisible || !canvasRef.current) return; let engine = null, scene = null; try { engine = new Engine(canvasRef.current, true, { stencil: false, antialias: true }); scene = new Scene(engine); scene.clearColor = new Color4(0.05, 0.07, 0.12, 1); const camera = new ArcRotateCamera('cam', -Math.PI / 2.5, Math.PI / 2.6, 8.5, new Vector3(0, 2.2, 0), scene); camera.attachControl(canvasRef.current, false); camera.minZ = 0.1; camera.maxZ = 50; new HemisphericLight('hemi', new Vector3(0, 1, 0), scene).intensity = 0.7; const sun = new DirectionalLight('sun', new Vector3(-0.5, -1, -0.3), scene); sun.intensity = 0.8; const floor = MeshBuilder.CreateGround('floor', { width: 7, height: 4 }, scene); const fmat = new StandardMaterial('fmat', scene); fmat.diffuseColor = new Color3(0.15, 0.20, 0.18); floor.material = fmat; const handle = fin.make(scene, `prev_${fin.id}`); if (handle && handle.root) handle.root.position.y = 0; scene.onBeforeRenderObservable.add(() => { if (handle && handle.root) handle.root.rotation.y += 0.005; }); engine.runRenderLoop(() => scene.render()); return () => { try { engine && engine.stopRenderLoop(); } catch (e) {} try { handle && handle.dispose && handle.dispose(); } catch (e) {} try { scene && scene.dispose(); } catch (e) {} try { engine && engine.dispose(); } catch (e) {} }; } catch (e) { console.warn('[FinCard] init failed', e); return () => {}; } }, [isVisible, fin]); return (
{isVisible ? :
•••
}
{fin.title}
{fin.id}
{isChosen && }
); } export default function GdFinishesPreview() { const { userRole, isLoading } = useAuth(); const navigate = useNavigate(); const [choices, setChoices] = useState({}); const [status, setStatus] = useState('idle'); useEffect(() => { const uid = getUserId(); if (!uid) return; api.get(`/kubikon3d/savegame/${CHOICES_PID}/${uid}/${CHOICES_NS}`) .then((r) => setChoices(r.data?.data || {})) .catch(() => {}); }, []); const choose = (epoch, finId) => { setChoices(prev => ({ ...prev, [epoch]: finId })); setStatus('idle'); }; const save = async () => { const uid = getUserId(); if (!uid) { setStatus('error'); return; } setStatus('loading'); try { await api.post(`/kubikon3d/savegame/${CHOICES_PID}/${uid}/${CHOICES_NS}`, { data: choices }); setStatus('saved'); } catch (e) { console.warn('[GdFinishesPreview] save failed', e); setStatus('error'); } }; if (isLoading) return
Загрузка...
; if (userRole !== 'admin') { return (

Доступ только для администратора

); } const chosenCount = Object.keys(choices).length; return (

GD — Финишные ворота ({FINISH_CATALOG.length})

Выбрано: {chosenCount}/10 эпох
{status === 'error' && Ошибка}
{EPOCH_INFO.map(epoch => { const fins = getFinishesByEpoch(epoch.n); const chosenId = choices[epoch.n]; return (

{epoch.emoji} Эпоха {epoch.n} — {epoch.name} L{(epoch.n - 1) * 10 + 1} – L{epoch.n * 10} {chosenId && (✓ выбран: {chosenId})}

{fins.map(fin => ( choose(epoch.n, fin.id)} /> ))}
); })}
); } const cardStyle = { background: '#0e1525', borderRadius: 10, overflow: 'hidden', transition: 'all 0.15s' }; const btnStyle = { padding: '10px 18px', background: '#2a3146', color: '#cdd4e0', border: 'none', borderRadius: 8, cursor: 'pointer', fontWeight: 600, fontSize: 14 }; const btnPrimary = { padding: '10px 24px', background: 'linear-gradient(135deg, #22ff66, #44aaff)', color: '#000', border: 'none', borderRadius: 8, cursor: 'pointer', fontWeight: 700, fontSize: 15 };