feat: update right widget preview

This commit is contained in:
2026-04-10 16:09:48 +03:00
parent fbf8232ce3
commit d380b2570f
5 changed files with 840 additions and 161 deletions

View File

@@ -0,0 +1,267 @@
@import url("https://fonts.googleapis.com/css2?family=Roboto:wght@400;600&display=swap");
.sfp-sight-frame {
display: flex;
flex-direction: column;
align-items: center;
width: 550px;
border-radius: 10px;
background:
linear-gradient(
114deg,
rgba(255, 255, 255, 0) 8.71%,
rgba(255, 255, 255, 0.16) 69.69%
),
#806c59;
}
.sfp-sight-frame-media-stack {
margin-top: 2px;
border-radius: 10px 10px 0 0;
position: relative;
width: calc(100% - 4px);
height: 300px;
overflow: hidden;
background: #111;
}
.sfp-sight-frame-media-item {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: fill;
opacity: 1;
z-index: 1;
pointer-events: auto;
}
.sfp-sight-frame-media-item video {
width: 100%;
height: 100%;
object-fit: fill;
}
.sfp-sight-frame-media-placeholder {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
color: rgba(255, 255, 255, 0.3);
font-family: "Roboto";
font-size: 14px;
}
.sfp-sight-frame-content {
display: flex;
flex-direction: column;
flex-grow: 1;
width: 100%;
}
.sfp-sight-frame-title {
display: flex;
align-items: center;
padding: 7px 16px;
width: 100%;
text-align: left;
font-family: "Roboto";
font-size: 24px;
font-weight: 600;
line-height: 120%;
border-bottom: 1px solid rgba(255, 255, 255, 0.8);
background:
linear-gradient(
180deg,
rgba(255, 255, 255, 0.22) 0%,
rgba(255, 255, 255, 0.04) 100%
),
rgba(179, 165, 152, 0.72);
box-shadow: 4px 4px 12px 0px rgba(255, 255, 255, 0.12) inset;
box-sizing: border-box;
color: white;
word-wrap: break-word;
overflow-wrap: break-word;
min-width: 0;
}
.sfp-sight-frame-title p {
word-wrap: break-word;
overflow-wrap: break-word;
word-break: break-word;
white-space: normal !important;
margin: 0;
width: 100%;
flex: 1;
min-width: 0;
max-width: 100%;
}
.sfp-intro-title {
height: 300px;
justify-content: center;
align-items: center;
border-bottom: none;
text-align: center;
font-size: 40px;
font-weight: 600;
line-height: 150%;
}
.sfp-sight-frame-text-wrapper {
flex-grow: 1;
padding: 16px;
box-sizing: border-box;
max-height: calc(80vh - 354px);
min-height: 80px;
overflow-y: auto;
display: flex;
flex-direction: column;
}
.sfp-sight-frame-text-wrapper::-webkit-scrollbar {
width: 5px;
}
.sfp-sight-frame-text-wrapper::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.1);
border-radius: 4px;
}
.sfp-sight-frame-text-wrapper::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.4);
border-radius: 1px;
}
.sfp-sight-frame-text {
padding-right: 10px;
text-align: left;
color: #fff;
font-family: "Roboto";
font-size: 18px;
font-style: normal;
font-weight: 400;
line-height: 150%;
margin-bottom: 0;
}
.sfp-sight-frame-text p,
.sfp-sight-frame-text h1,
.sfp-sight-frame-text h2,
.sfp-sight-frame-text h3,
.sfp-sight-frame-text li {
color: #fff;
}
.sfp-sight-frame-menu {
position: relative;
padding: 7px;
width: 100%;
display: flex;
align-items: center;
justify-content: space-around;
border-radius: 0px 0px 10px 10px;
border-top: 1px solid rgba(255, 255, 255, 0.8);
background:
linear-gradient(
180deg,
rgba(255, 255, 255, 0.2) 0%,
rgba(255, 255, 255, 0) 100%
),
rgba(179, 165, 152, 0.4);
box-shadow: 4px 4px 12px 0px rgba(255, 255, 255, 0.12) inset;
backdrop-filter: blur(10px);
box-sizing: border-box;
flex-shrink: 0;
}
.sfp-sight-frame-menu-point {
background: none;
border: none;
color: #fff;
text-align: center;
font-family: "Roboto";
font-size: 18px;
font-style: normal;
font-weight: 400;
padding: 8px 12px;
white-space: nowrap;
cursor: pointer;
transition:
background-color 0.1s ease,
color 0.1s ease;
}
.sfp-sight-frame-menu-point.active {
border-bottom: 2px solid #fff;
font-weight: 600;
}
.sfp-back-btn {
position: absolute;
left: 10px;
margin-top: -4.5px;
z-index: 1;
padding: 4.5px 7.5px 4.5px 15px;
cursor: pointer;
background: none;
border: none;
}
.sfp-watermark {
position: absolute;
width: 51px;
height: auto;
left: 8px;
top: 8px;
z-index: 10;
}
.sfp-three-d-controls-bar {
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 44px;
display: flex;
flex-direction: row;
align-items: center;
gap: 15px;
padding: 0 30px;
background: rgba(64, 64, 64, 0.6);
z-index: 10;
pointer-events: none;
}
.sfp-three-d-controls-bar .sfp-three-d-control-btn {
display: flex;
align-items: center;
justify-content: center;
background: transparent;
border: none;
cursor: pointer;
padding: 0;
width: min-content;
pointer-events: auto;
color: white;
transition: opacity 0.2s ease;
}
.sfp-three-d-controls-bar .sfp-three-d-control-btn:hover {
opacity: 0.85;
}
.sfp-three-d-controls-bar .sfp-three-d-control-btn svg {
width: 28px;
height: 28px;
flex-shrink: 0;
}
.sfp-sight-frame-media-stack.three-d-view {
background-color: #111 !important;
}

View File

@@ -0,0 +1,326 @@
import { useMemo, useRef, useState } from "react";
import { ReactPhotoSphereViewer } from "react-photo-sphere-viewer";
import { ReactMarkdownComponent } from "../../ReactMarkdown";
import { ThreeViewErrorBoundary } from "../../MediaViewer/ThreeViewErrorBoundary";
import {
SightFrameThreeView,
ThreeViewHandle,
} from "./SightFrameThreeView";
import { MinusIcon, PlusIcon } from "./SightFrameThreeViewIcons";
import "./SightFramePreview.css";
interface ArticleMedia {
id: string;
media_type: number;
filename: string;
}
interface Article {
id: number;
heading: string;
body: string;
media: ArticleMedia[];
}
interface PreviewMediaData {
id: string;
media_type: number;
filename?: string;
}
interface SightFramePreviewProps {
sightName: string;
previewMedia: PreviewMediaData | null;
articles: Article[];
onArticleSelect: (index: number) => void;
}
// Matches SightFrame.jsx renderCurrentMedia — same structure, same CSS classes
function renderCurrentMedia(
media: PreviewMediaData | ArticleMedia | null,
token: string,
threeViewResetKey: number,
threeViewControlRef: React.MutableRefObject<ThreeViewHandle | null>,
onZoomIn: () => void,
onZoomOut: () => void,
onThreeViewReset: () => void
) {
if (!media) return null;
const src = `${import.meta.env.VITE_KRBL_MEDIA}${media.id}/download?token=${token}`;
const className = "sfp-sight-frame-media-item";
switch (media.media_type) {
// image (1), gif (3), sprite (4)
case 1:
case 3:
case 4:
return (
<img
src={src}
alt=""
className={className}
onError={(e) => {
(e.target as HTMLImageElement).style.display = "none";
}}
/>
);
// video
case 2:
return (
<video
muted
autoPlay
loop
playsInline
className={className}
onError={(e) => {
(e.target as HTMLVideoElement).style.display = "none";
}}
>
<source src={src} type="video/mp4" />
</video>
);
// panorama
case 5:
return (
<div
className={`${className} panorama-container`}
style={{ isolation: "isolate" }}
>
<ReactPhotoSphereViewer
src={src}
width={"100%"}
height={"100%"}
/>
</div>
);
// 3d
case 6:
return (
<div className={className} style={{ position: "relative" }}>
<ThreeViewErrorBoundary
resetKey={`sfp-${media.id}-${threeViewResetKey}`}
onReset={onThreeViewReset}
>
<SightFrameThreeView
key={`sfp-3d-${media.id}-${threeViewResetKey}`}
fileUrl={src}
width={"100%"}
height={"100%"}
controlRef={threeViewControlRef}
/>
</ThreeViewErrorBoundary>
<div className="sfp-three-d-controls-bar">
<button
type="button"
className="sfp-three-d-control-btn"
title="Уменьшить"
onPointerUp={onZoomOut}
>
<MinusIcon />
</button>
<button
type="button"
className="sfp-three-d-control-btn"
title="Увеличить"
onPointerUp={onZoomIn}
>
<PlusIcon />
</button>
</div>
</div>
);
default:
return (
<div className={className}>
<div className="sfp-sight-frame-media-placeholder">
Неподдерживаемый тип медиа
</div>
</div>
);
}
}
export function SightFramePreview({
sightName,
previewMedia,
articles,
onArticleSelect,
}: SightFramePreviewProps) {
const token = localStorage.getItem("token") ?? "";
// -1 = intro (section 0 in SightFrame)
const [selectedSection, setSelectedSection] = useState<number>(-1);
const [threeViewResetKey, setThreeViewResetKey] = useState(0);
const threeViewControlRef = useRef<ThreeViewHandle | null>(null);
const currentArticle =
selectedSection >= 0 && selectedSection < articles.length
? articles[selectedSection]
: null;
const currentMedia: PreviewMediaData | ArticleMedia | null = currentArticle
? (currentArticle.media[0] ?? null)
: previewMedia;
const isCurrentMedia3D = currentMedia?.media_type === 6;
// Replicates processedSightName from SightFrame.jsx
const processedSightName = useMemo(() => {
if (!sightName) return sightName;
const namePattern =
/([А-Яа-яA-Za-z0-9]\. [А-Яа-яA-Za-z0-9]\. [А-Яа-яA-Za-z0-9]+)/g;
const parts = sightName.split(namePattern);
if (parts.length > 1) {
return parts.map((part, index) => {
const initialsPattern =
/^[А-Яа-яA-Za-z0-9]\. [А-Яа-яA-Za-z0-9]\. [А-Яа-яA-Za-z0-9]+$/;
if (initialsPattern.test(part)) {
return (
<span key={index}>
<br />
{part}
</span>
);
}
return <span key={index}>{part}</span>;
});
}
return sightName;
}, [sightName]);
// Replicates titleLineHeight from SightFrame.jsx
const titleLineHeight = useMemo(() => {
if (!sightName) return "120%";
const textLength = sightName.length;
const calculatedLineHeight = Math.max(
100,
Math.min(120, 120 - (textLength / 10) * 1)
);
return `${calculatedLineHeight}%`;
}, [sightName]);
const isIntro = selectedSection === -1;
return (
<div className="sfp-sight-frame">
{/* media stack — always rendered, matches .sight-frame-media-stack */}
<div
className={`sfp-sight-frame-media-stack${isCurrentMedia3D ? " three-d-view" : ""}`}
>
{currentMedia ? (
renderCurrentMedia(
currentMedia,
token,
threeViewResetKey,
threeViewControlRef,
() => threeViewControlRef.current?.zoomIn?.(),
() => threeViewControlRef.current?.zoomOut?.(),
() => setThreeViewResetKey((k) => k + 1)
)
) : (
<div className="sfp-sight-frame-media-placeholder">
{isIntro ? "Медиа не добавлено" : "Нет медиа"}
</div>
)}
</div>
{/* content */}
<div className="sfp-sight-frame-content">
{/* title: intro-title (300px, 40px centered) or regular (24px with border) */}
<div
className={`sfp-sight-frame-title${isIntro ? " sfp-intro-title" : ""}`}
style={{ lineHeight: isIntro ? undefined : titleLineHeight }}
>
<p
style={{
whiteSpace: "normal",
wordBreak: "break-word",
overflowWrap: "break-word",
}}
>
{isIntro ? processedSightName : sightName}
</p>
</div>
{/* text body — only for article sections */}
{!isIntro && currentArticle && (
<div className="sfp-sight-frame-text-wrapper">
<div className="sfp-sight-frame-text">
<ReactMarkdownComponent value={currentArticle.body} />
</div>
</div>
)}
</div>
{/* menu — matches .sight-frame-menu */}
<div className="sfp-sight-frame-menu">
{/* back arrow — shown when not on intro */}
{!isIntro && (
<button
className="sfp-back-btn"
type="button"
onClick={() => setSelectedSection(-1)}
>
<svg
width="20"
height="25"
viewBox="0 0 20 25"
fill="none"
xmlns="http://www.w3.org/2000/svg"
style={{ display: "block" }}
>
<defs>
<linearGradient
id="sfpGradient"
x1="0%"
y1="0%"
x2="100%"
y2="100%"
gradientUnits="userSpaceOnUse"
>
<stop offset="0%" stopColor="rgba(255, 255, 255, 0.2)" />
<stop offset="100%" stopColor="rgba(255, 255, 255, 0)" />
</linearGradient>
<clipPath id="sfpClip">
<rect
width="20"
height="25"
fill="white"
transform="translate(12.5 0.5) rotate(90)"
/>
</clipPath>
</defs>
<g clipPath="url(#sfpClip)">
<path
d="M4.03158 11.0738C4.21879 11.2087 4.34702 11.2766 4.44531 11.3747C6.93048 13.8687 9.41098 16.3665 11.8962 18.8605C12.3408 19.3067 12.5186 19.8207 12.3361 20.4339C12.0281 21.4658 10.7776 21.8393 9.95295 21.1498C9.86309 21.0743 9.78165 20.9894 9.69928 20.9064C6.85279 18.0446 4.00537 15.1817 1.15982 12.3189C0.280876 11.4341 0.281813 10.6456 1.16169 9.75982C4.04 6.86305 6.91457 3.96155 9.80786 1.07986C10.0597 0.828952 10.4144 0.619547 10.7561 0.537482C11.4019 0.382786 12.015 0.72142 12.3193 1.28644C12.6263 1.85334 12.5392 2.56079 12.0806 3.05129C11.7286 3.4286 11.3561 3.78704 10.991 4.15209C8.79601 6.35557 6.60194 8.55904 4.40599 10.7606C4.32362 10.8436 4.22721 10.9106 4.03158 11.0729L4.03158 11.0738Z"
fill="url(#sfpGradient)"
/>
</g>
</svg>
</button>
)}
{/* article nav buttons — matches .sight-frame-menu-point */}
{articles.map((article, index) => (
<button
key={article.id}
type="button"
className={`sfp-sight-frame-menu-point${selectedSection === index ? " active" : ""}`}
onClick={() => {
setSelectedSection(index);
onArticleSelect(index);
}}
>
{article.heading}
</button>
))}
</div>
</div>
);
}

View File

@@ -0,0 +1,178 @@
import { Canvas, useThree } from "@react-three/fiber";
import { OrbitControls, Stage, useGLTF } from "@react-three/drei";
import React, { useEffect, Suspense } from "react";
import * as THREE from "three";
const BACKGROUND_COLOR = 0x111111;
export interface ThreeViewHandle {
zoomIn: () => void;
zoomOut: () => void;
}
interface ThreeViewProps {
fileUrl: string;
width?: string;
height?: string;
onLoad?: () => void;
onError?: (error: string) => void;
onAspectRatioCalculated?: (ratio: number) => void;
controlRef?: React.MutableRefObject<ThreeViewHandle | null>;
}
const ZOOM_FACTOR = 1.2;
const MIN_DISTANCE = 1;
const MAX_DISTANCE = 100;
const ZoomController = ({
controlRef,
}: {
controlRef?: React.MutableRefObject<ThreeViewHandle | null>;
}) => {
const { camera, controls } = useThree();
useEffect(() => {
if (!controlRef || !controls) return;
const orbit = controls as unknown as {
target: THREE.Vector3;
getDistance: () => number;
setDistance?: (d: number) => void;
};
const zoomIn = () => {
const target = orbit.target;
const dir = new THREE.Vector3()
.subVectors(camera.position, target)
.normalize();
const dist = camera.position.distanceTo(target);
const newDist = Math.max(MIN_DISTANCE, dist / ZOOM_FACTOR);
camera.position.copy(target).addScaledVector(dir, newDist);
camera.updateProjectionMatrix();
};
const zoomOut = () => {
const target = orbit.target;
const dir = new THREE.Vector3()
.subVectors(camera.position, target)
.normalize();
const dist = camera.position.distanceTo(target);
const newDist = Math.min(MAX_DISTANCE, dist * ZOOM_FACTOR);
camera.position.copy(target).addScaledVector(dir, newDist);
camera.updateProjectionMatrix();
};
controlRef.current = { zoomIn, zoomOut };
return () => {
controlRef.current = null;
};
}, [camera, controls, controlRef]);
return null;
};
const AutoResize = () => {
const { gl, camera } = useThree();
useEffect(() => {
const handleResize = () => {
const parent = gl.domElement.parentElement;
if (!parent) return;
gl.setSize(parent.clientWidth, parent.clientHeight);
if (camera instanceof THREE.PerspectiveCamera) {
camera.aspect = parent.clientWidth / parent.clientHeight;
camera.updateProjectionMatrix();
}
};
handleResize();
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
}, [gl, camera]);
return null;
};
const Model = ({
fileUrl,
onLoad,
}: {
fileUrl: string;
onLoad?: () => void;
}) => {
const { scene } = useGLTF(fileUrl);
useEffect(() => {
if (scene) {
scene.traverse((child) => {
if ((child as THREE.Mesh).isMesh) {
const m = (child as THREE.Mesh)
.material as THREE.MeshStandardMaterial;
if (m) {
m.envMapIntensity = 1.5;
m.needsUpdate = true;
}
}
});
onLoad?.();
}
}, [scene, onLoad]);
return <primitive object={scene} />;
};
export const SightFrameThreeView: React.FC<ThreeViewProps> = ({
fileUrl,
width = "100%",
height = "100%",
onLoad,
onError,
controlRef,
}) => {
return (
<div style={{ width, height, position: "relative", overflow: "hidden" }}>
<Canvas
gl={{
antialias: true,
toneMappingExposure: 1.5,
outputColorSpace: THREE.SRGBColorSpace,
}}
camera={{ position: [0, 0, 5], fov: 40 }}
style={{ width: "100%", height: "100%" }}
onError={() => onError?.("Canvas error")}
>
<AutoResize />
{controlRef && <ZoomController controlRef={controlRef} />}
<color attach="background" args={[BACKGROUND_COLOR]} />
<ambientLight intensity={0.8} />
<directionalLight position={[30, 30, 30]} intensity={1.2} />
<directionalLight position={[-30, 30, 30]} intensity={1.2} />
<directionalLight position={[30, -30, 30]} intensity={1} />
<directionalLight position={[-30, -30, 30]} intensity={1} />
<directionalLight position={[0, 0, 50]} intensity={1} />
<directionalLight position={[0, 0, -50]} intensity={0.8} />
<directionalLight position={[50, 0, 0]} intensity={0.8} />
<directionalLight position={[-50, 0, 0]} intensity={0.8} />
<directionalLight position={[0, 50, 0]} intensity={0.8} />
<directionalLight position={[0, -50, 0]} intensity={0.6} />
<pointLight position={[30, 30, 30]} intensity={1.2} />
<pointLight position={[-30, -30, -30]} intensity={1.2} />
<pointLight position={[0, 30, 0]} intensity={0.6} />
<Suspense fallback={null}>
<Stage
environment={null}
intensity={1}
shadows={false}
adjustCamera={true}
>
<Model fileUrl={fileUrl} onLoad={onLoad} />
</Stage>
</Suspense>
<OrbitControls
makeDefault
enableZoom={true}
enablePan={true}
target={[50, 50, 50]}
minDistance={1}
maxDistance={100}
enableDamping={true}
/>
</Canvas>
</div>
);
};

View File

@@ -0,0 +1,55 @@
import React from "react";
export const MinusIcon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => (
<svg
width="32"
height="32"
viewBox="0 0 32 32"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<g clipPath="url(#clip0_minus)">
<path
d="M22.4688 19.552C24.2685 17.0223 25.0521 13.9089 24.6644 10.8287C24.2766 7.74852 22.7459 4.9264 20.3754 2.92166C18.005 0.916911 14.968 -0.124135 11.8661 0.00483817C8.76432 0.133812 5.82415 1.42339 3.62834 3.618C1.43253 5.81261 0.141333 8.75207 0.0106576 11.8538C-0.120018 14.9556 0.919362 17.9932 2.92281 20.3647C4.92625 22.7362 7.74753 24.2686 10.8275 24.658C13.9075 25.0474 17.0214 24.2655 19.552 22.4672L27.528 30.4432C28.3328 31.2496 29.64 31.2496 30.4432 30.4432C30.6353 30.2521 30.7877 30.025 30.8917 29.7749C30.9957 29.5247 31.0492 29.2565 31.0492 28.9856C31.0492 28.7147 30.9957 28.4465 30.8917 28.1963C30.7877 27.9462 30.6353 27.7191 30.4432 27.528L22.4688 19.552ZM12.3808 21.2208C11.2096 21.2366 10.0469 21.0195 8.9603 20.5822C7.87368 20.1449 6.88476 19.4961 6.05096 18.6734C5.21717 17.8507 4.55511 16.8706 4.10323 15.79C3.65135 14.7094 3.41865 13.5497 3.41865 12.3784C3.41865 11.2071 3.65135 10.0474 4.10323 8.9668C4.55511 7.88616 5.21717 6.90606 6.05096 6.0834C6.88476 5.26075 7.87368 4.61193 8.9603 4.17463C10.0469 3.73732 11.2096 3.52025 12.3808 3.536C14.7053 3.56727 16.9241 4.51264 18.5569 6.16751C20.1896 7.82238 21.1051 10.0536 21.1051 12.3784C21.1051 14.7032 20.1896 16.9344 18.5569 18.5893C16.9241 20.2442 14.7053 21.1895 12.3808 21.2208Z"
fill="white"
/>
<path
d="M6.4096 10.7939H18.344V13.9683H6.4096V10.7939Z"
fill="white"
/>
</g>
<defs>
<clipPath id="clip0_minus">
<rect width="32" height="32" fill="white" />
</clipPath>
</defs>
</svg>
);
export const PlusIcon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => (
<svg
width="32"
height="32"
viewBox="0 0 32 32"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<g clipPath="url(#clip0_plus)">
<path
d="M22.4688 19.552C24.2685 17.0223 25.0521 13.9089 24.6643 10.8287C24.2766 7.74852 22.7459 4.9264 20.3754 2.92166C18.005 0.916911 14.968 -0.124135 11.8661 0.00483817C8.76432 0.133812 5.82415 1.42339 3.62834 3.618C1.43253 5.81261 0.141332 8.75207 0.0106566 11.8538C-0.120019 14.9556 0.919361 17.9932 2.9228 20.3647C4.92625 22.7362 7.74753 24.2686 10.8275 24.658C13.9075 25.0474 17.0214 24.2655 19.552 22.4672L27.528 30.4432C28.3328 31.2496 29.64 31.2496 30.4432 30.4432C30.6353 30.2521 30.7877 30.025 30.8917 29.7749C30.9957 29.5247 31.0492 29.2565 31.0492 28.9856C31.0492 28.7147 30.9957 28.4465 30.8917 28.1963C30.7877 27.9462 30.6353 27.7191 30.4432 27.528L22.4688 19.552ZM12.3808 21.2208C11.2096 21.2366 10.0469 21.0195 8.9603 20.5822C7.87368 20.1449 6.88476 19.4961 6.05096 18.6734C5.21717 17.8507 4.55511 16.8706 4.10323 15.79C3.65135 14.7094 3.41865 13.5497 3.41865 12.3784C3.41865 11.2071 3.65135 10.0474 4.10323 8.9668C4.55511 7.88616 5.21717 6.90606 6.05096 6.0834C6.88476 5.26075 7.87368 4.61193 8.9603 4.17463C10.0469 3.73732 11.2096 3.52025 12.3808 3.536C14.7053 3.56727 16.9241 4.51264 18.5569 6.16751C20.1896 7.82238 21.1051 10.0536 21.1051 12.3784C21.1051 14.7032 20.1896 16.9344 18.5569 18.5893C16.9241 20.2442 14.7053 21.1895 12.3808 21.2208Z"
fill="white"
/>
<path
d="M13.9648 6.41406H10.7904V10.7933H6.4096V13.9677H10.7904V18.3453H13.9648V13.9677H18.344V10.7933H13.9648V6.41406Z"
fill="white"
/>
</g>
<defs>
<clipPath id="clip0_plus">
<rect width="32" height="32" fill="white" />
</clipPath>
</defs>
</svg>
);

View File

@@ -1,7 +1,6 @@
import {
Box,
Button,
Paper,
Typography,
Menu,
MenuItem,
@@ -22,10 +21,9 @@ import {
LanguageSwitcher,
MediaArea,
MediaAreaForSight,
ReactMarkdownComponent,
ReactMarkdownEditor,
} from "@widgets";
import { ImagePlus, Plus, Save, Trash2, Unlink, X } from "lucide-react";
import { Plus, Save, Trash2, Unlink, X } from "lucide-react";
import { observer } from "mobx-react-lite";
import { useEffect, useState } from "react";
import { toast } from "react-toastify";
@@ -36,6 +34,7 @@ import {
Draggable,
DropResult,
} from "@hello-pangea/dnd";
import { SightFramePreview } from "./SightFramePreview";
export const RightWidgetTab = observer(
({ value, index }: { value: number; index: number }) => {
@@ -213,7 +212,7 @@ export const RightWidgetTab = observer(
</div>
<Box sx={{ display: "flex", flexGrow: 1, gap: 2.5 }}>
<Box className="flex flex-col w-[75%] gap-2">
<Box className="flex flex-col gap-2" sx={{ flexGrow: 1, minWidth: 0 }}>
<Box className="w-full flex gap-2">
<Box className="relative w-[20%] h-[70vh] flex flex-col rounded-2xl overflow-y-auto gap-3 border border-gray-300 p-3">
<Box className="flex flex-col gap-3 max-h-[60vh] overflow-y-auto">
@@ -455,163 +454,17 @@ export const RightWidgetTab = observer(
</Box>
</Box>
{type === "article" && (
<Box className="w-[25%] mr-10">
{activeArticleIndex !== null && (
<Paper
className="flex-1 flex flex-col max-w-[500px]"
sx={{
borderRadius: "10px",
overflow: "hidden",
}}
elevation={2}
>
<Box
className=" overflow-hidden"
sx={{
width: "100%",
background: "#877361",
borderColor: "grey.300",
display: "flex",
flexDirection: "column",
}}
>
{sight[language].right[activeArticleIndex].media.length >
0 ? (
<Box
sx={{
overflow: "hidden",
width: "100%",
padding: "2px 2px 0px 2px",
"& img": {
borderTopLeftRadius: "10px",
borderTopRightRadius: "10px",
width: "100%",
height: "auto",
objectFit: "contain",
},
}}
>
<MediaViewer
media={
sight[language].right[activeArticleIndex].media[0]
}
fullWidth
/>
</Box>
) : (
<Box
sx={{
width: "100%",
height: 200,
flexShrink: 0,
backgroundColor: "rgba(0,0,0,0.1)",
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<ImagePlus size={48} color="white" />
</Box>
)}
<Box
sx={{
p: 1,
wordBreak: "break-word",
fontSize: "24px",
fontWeight: 700,
lineHeight: "120%",
backdropFilter: "blur(12px)",
borderBottom: "1px solid #A89F90",
boxShadow:
"inset 4px 4px 12px 0 rgba(255,255,255,0.12)",
background:
"#806c59 linear-gradient(90deg, rgba(255, 255, 255, 0.2) 12.5%, rgba(255, 255, 255, 0.2) 100%)",
}}
>
<Typography variant="h6" color="white">
{sight[language].right[activeArticleIndex].heading ||
"Выберите статью"}
</Typography>
</Box>
<Box
sx={{
padding: 1,
minHeight: "200px",
maxHeight: "300px",
overflowY: "auto",
width: "100%",
"&::-webkit-scrollbar": {
display: "none",
},
"&": {
scrollbarWidth: "none",
},
background:
"rgba(179, 165, 152, 0.4), linear-gradient(180deg, rgba(255, 255, 255, 0.2) 0%, rgba(255, 255, 255, 0.2) 100%)",
flexGrow: 1,
}}
>
{sight[language].right[activeArticleIndex].body ? (
<ReactMarkdownComponent
value={
sight[language].right[activeArticleIndex].body
}
/>
) : (
<Typography
color="rgba(255,255,255,0.7)"
sx={{ textAlign: "center", mt: 4 }}
>
Предпросмотр статьи появится здесь
</Typography>
)}
</Box>
<Box
sx={{
p: 2,
display: "flex",
justifyContent: "center",
fontSize: "24px",
fontWeight: 700,
lineHeight: "120%",
flexWrap: "wrap",
gap: "34px",
backdropFilter: "blur(12px)",
boxShadow:
"inset 4px 4px 12px 0 rgba(255,255,255,0.12)",
background:
"#806c59 linear-gradient(90deg, rgba(255, 255, 255, 0.2) 12.5%, rgba(255, 255, 255, 0.2) 100%)",
}}
>
{sight[language].right.length > 0 &&
sight[language].right.map((article, index) => (
<button
className={`inline-block text-left text-xs text-white ${
activeArticleIndex === index ? "underline" : ""
}`}
onClick={() => {
handleSelectArticle(index);
setType("article");
}}
>
{article.heading}
</button>
))}
</Box>
</Box>
</Paper>
)}
</Box>
)}
<Box sx={{ flexShrink: 0, width: "550px" }}>
<SightFramePreview
sightName={sight[language].name}
previewMedia={previewMedia}
articles={sight[language].right}
onArticleSelect={(idx) => {
handleSelectArticle(idx);
setType("article");
}}
/>
</Box>
</Box>
<Box