#14 Перепись редактирования и создания маршрута #16
@@ -71,10 +71,8 @@ export const clearBlobAndGLTFCache = async (url: string) => {
 | 
				
			|||||||
 */
 | 
					 */
 | 
				
			||||||
export const clearMediaTransitionCache = async (
 | 
					export const clearMediaTransitionCache = async (
 | 
				
			||||||
  previousMediaId: string | number | null,
 | 
					  previousMediaId: string | number | null,
 | 
				
			||||||
  newMediaId: string | number | null,
 | 
					 | 
				
			||||||
  newMediaType?: number
 | 
					  newMediaType?: number
 | 
				
			||||||
) => {
 | 
					) => {
 | 
				
			||||||
  console.log(newMediaId, newMediaType);
 | 
					 | 
				
			||||||
  // Если переключаемся с/на 3D модель, очищаем весь кеш
 | 
					  // Если переключаемся с/на 3D модель, очищаем весь кеш
 | 
				
			||||||
  if (newMediaType === 6 || previousMediaId) {
 | 
					  if (newMediaType === 6 || previousMediaId) {
 | 
				
			||||||
    await clearAllGLTFCache();
 | 
					    await clearAllGLTFCache();
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -523,42 +523,60 @@ export const ArticleSelectOrCreateDialog = observer(
 | 
				
			|||||||
      article?.service_name?.toLowerCase().includes(searchQuery.toLowerCase())
 | 
					      article?.service_name?.toLowerCase().includes(searchQuery.toLowerCase())
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const [hoveredArticleId, setHoveredArticleId] = useState<string | null>(
 | 
					    // Preview-by-click logic with request serialization to avoid concurrent requests
 | 
				
			||||||
      null
 | 
					    const [isPreviewLoading, setIsPreviewLoading] = useState(false);
 | 
				
			||||||
    );
 | 
					    const [queuedPreviewId, setQueuedPreviewId] = useState<number | null>(null);
 | 
				
			||||||
    const hoverTimerRef = (typeof window !== "undefined"
 | 
					    const clickTimerRef = (typeof window !== "undefined"
 | 
				
			||||||
      ? (window as any)
 | 
					      ? (window as any)
 | 
				
			||||||
      : {}) as {
 | 
					      : {}) as {
 | 
				
			||||||
      current?: any;
 | 
					      current?: any;
 | 
				
			||||||
    } as React.MutableRefObject<NodeJS.Timeout | null>;
 | 
					    } as React.MutableRefObject<NodeJS.Timeout | null>;
 | 
				
			||||||
    if (hoverTimerRef.current === undefined) {
 | 
					    if (clickTimerRef.current === undefined) {
 | 
				
			||||||
      (hoverTimerRef as any).current = null;
 | 
					      (clickTimerRef as any).current = null;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    useEffect(() => {
 | 
					
 | 
				
			||||||
      if (
 | 
					    const runPreviewFetch = async (articleId: number) => {
 | 
				
			||||||
        hoveredArticleId &&
 | 
					      if (isPreviewLoading) {
 | 
				
			||||||
        tabValue === 0 &&
 | 
					        setQueuedPreviewId(articleId);
 | 
				
			||||||
        !selectedArticleId &&
 | 
					        return;
 | 
				
			||||||
        !tempArticleId
 | 
					 | 
				
			||||||
      ) {
 | 
					 | 
				
			||||||
        if (hoverTimerRef.current) clearTimeout(hoverTimerRef.current);
 | 
					 | 
				
			||||||
        hoverTimerRef.current = setTimeout(() => {
 | 
					 | 
				
			||||||
          getArticle(Number(hoveredArticleId), modalLanguage);
 | 
					 | 
				
			||||||
          getArticleMedia(Number(hoveredArticleId));
 | 
					 | 
				
			||||||
        }, 200);
 | 
					 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      return () => {
 | 
					      setIsPreviewLoading(true);
 | 
				
			||||||
        if (hoverTimerRef.current) clearTimeout(hoverTimerRef.current);
 | 
					      try {
 | 
				
			||||||
      };
 | 
					        await Promise.all([
 | 
				
			||||||
    }, [
 | 
					          getArticle(articleId, modalLanguage),
 | 
				
			||||||
      hoveredArticleId,
 | 
					          getArticleMedia(articleId),
 | 
				
			||||||
      tabValue,
 | 
					 | 
				
			||||||
      selectedArticleId,
 | 
					 | 
				
			||||||
      tempArticleId,
 | 
					 | 
				
			||||||
      modalLanguage,
 | 
					 | 
				
			||||||
      getArticle,
 | 
					 | 
				
			||||||
      getArticleMedia,
 | 
					 | 
				
			||||||
        ]);
 | 
					        ]);
 | 
				
			||||||
 | 
					      } finally {
 | 
				
			||||||
 | 
					        setIsPreviewLoading(false);
 | 
				
			||||||
 | 
					        if (queuedPreviewId && queuedPreviewId !== articleId) {
 | 
				
			||||||
 | 
					          const nextId = queuedPreviewId;
 | 
				
			||||||
 | 
					          setQueuedPreviewId(null);
 | 
				
			||||||
 | 
					          // Run the next queued preview
 | 
				
			||||||
 | 
					          runPreviewFetch(nextId);
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          setQueuedPreviewId(null);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const handleListItemClick = (articleId: number) => {
 | 
				
			||||||
 | 
					      // Delay to allow double-click to cancel preview
 | 
				
			||||||
 | 
					      if (clickTimerRef.current) clearTimeout(clickTimerRef.current);
 | 
				
			||||||
 | 
					      clickTimerRef.current = setTimeout(() => {
 | 
				
			||||||
 | 
					        if (tabValue === 0 && !selectedArticleId && !tempArticleId) {
 | 
				
			||||||
 | 
					          runPreviewFetch(articleId);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }, 200);
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const handleListItemDoubleClick = (articleId: number) => {
 | 
				
			||||||
 | 
					      // Cancel pending single-click preview and proceed to select
 | 
				
			||||||
 | 
					      if (clickTimerRef.current) {
 | 
				
			||||||
 | 
					        clearTimeout(clickTimerRef.current);
 | 
				
			||||||
 | 
					        (clickTimerRef as any).current = null;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      handleArticleSelect(articleId);
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const previewData = {
 | 
					    const previewData = {
 | 
				
			||||||
      heading: currentArticleData[modalLanguage].heading || "",
 | 
					      heading: currentArticleData[modalLanguage].heading || "",
 | 
				
			||||||
@@ -656,11 +674,10 @@ export const ArticleSelectOrCreateDialog = observer(
 | 
				
			|||||||
                        filteredArticles.map((article) => (
 | 
					                        filteredArticles.map((article) => (
 | 
				
			||||||
                          <ListItemButton
 | 
					                          <ListItemButton
 | 
				
			||||||
                            key={article.id}
 | 
					                            key={article.id}
 | 
				
			||||||
                            onClick={() => handleArticleSelect(article.id)}
 | 
					                            onClick={() => handleListItemClick(article.id)}
 | 
				
			||||||
                            onMouseEnter={() =>
 | 
					                            onDoubleClick={() =>
 | 
				
			||||||
                              setHoveredArticleId(article.id.toString())
 | 
					                              handleListItemDoubleClick(article.id)
 | 
				
			||||||
                            }
 | 
					                            }
 | 
				
			||||||
                            onMouseLeave={() => setHoveredArticleId(null)}
 | 
					 | 
				
			||||||
                            sx={{
 | 
					                            sx={{
 | 
				
			||||||
                              borderRadius: 1,
 | 
					                              borderRadius: 1,
 | 
				
			||||||
                              mb: 0.5,
 | 
					                              mb: 0.5,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -340,55 +340,63 @@ class CreateSightStore {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  createLeftArticle = async () => {
 | 
					  createLeftArticle = async () => {
 | 
				
			||||||
    /* ... your existing logic to create a new left article (placeholder or DB) ... */
 | 
					    /* ... your existing logic to create a new left article (placeholder or DB) ... */
 | 
				
			||||||
 | 
					    const ruName = (this.sight.ru.name || "").trim();
 | 
				
			||||||
 | 
					    const enName = (this.sight.en.name || "").trim();
 | 
				
			||||||
 | 
					    const zhName = (this.sight.zh.name || "").trim();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // If all names are empty, skip defaulting and use empty headings
 | 
				
			||||||
 | 
					    const hasAnyName = !!(ruName || enName || zhName);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const response = await languageInstance("ru").post("/article", {
 | 
					    const response = await languageInstance("ru").post("/article", {
 | 
				
			||||||
      heading: "Новая левая статья",
 | 
					      heading: hasAnyName ? ruName : "",
 | 
				
			||||||
      body: "Заполните контентом",
 | 
					      body: "",
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
    const newLeftArticleId = response.data.id;
 | 
					    const newLeftArticleId = response.data.id;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    await languageInstance("en").patch(`/article/${newLeftArticleId}`, {
 | 
					    await languageInstance("en").patch(`/article/${newLeftArticleId}`, {
 | 
				
			||||||
      heading: "New Left Article",
 | 
					      heading: hasAnyName ? enName : "",
 | 
				
			||||||
      body: "Fill with content",
 | 
					      body: "",
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
    await languageInstance("zh").patch(`/article/${newLeftArticleId}`, {
 | 
					    await languageInstance("zh").patch(`/article/${newLeftArticleId}`, {
 | 
				
			||||||
      heading: "新的左侧文章",
 | 
					      heading: hasAnyName ? zhName : "",
 | 
				
			||||||
      body: "填写内容",
 | 
					      body: "",
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    runInAction(() => {
 | 
					    runInAction(() => {
 | 
				
			||||||
      this.sight.left_article = newLeftArticleId; // Store the actual ID
 | 
					      this.sight.left_article = newLeftArticleId; // Store the actual ID
 | 
				
			||||||
      this.sight.ru.left = {
 | 
					      this.sight.ru.left = {
 | 
				
			||||||
        heading: "Новая левая статья",
 | 
					        heading: hasAnyName ? ruName : "",
 | 
				
			||||||
        body: "Заполните контентом",
 | 
					        body: "",
 | 
				
			||||||
        media: [],
 | 
					        media: [],
 | 
				
			||||||
      };
 | 
					      };
 | 
				
			||||||
      this.sight.en.left = {
 | 
					      this.sight.en.left = {
 | 
				
			||||||
        heading: "New Left Article",
 | 
					        heading: hasAnyName ? enName : "",
 | 
				
			||||||
        body: "Fill with content",
 | 
					        body: "",
 | 
				
			||||||
        media: [],
 | 
					        media: [],
 | 
				
			||||||
      };
 | 
					      };
 | 
				
			||||||
      this.sight.zh.left = {
 | 
					      this.sight.zh.left = {
 | 
				
			||||||
        heading: "新的左侧文章",
 | 
					        heading: hasAnyName ? zhName : "",
 | 
				
			||||||
        body: "填写内容",
 | 
					        body: "",
 | 
				
			||||||
        media: [],
 | 
					        media: [],
 | 
				
			||||||
      };
 | 
					      };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      articlesStore.articles.ru.push({
 | 
					      articlesStore.articles.ru.push({
 | 
				
			||||||
        id: newLeftArticleId,
 | 
					        id: newLeftArticleId,
 | 
				
			||||||
        heading: "Новая левая статья",
 | 
					        heading: hasAnyName ? ruName : "",
 | 
				
			||||||
        body: "Заполните контентом",
 | 
					        body: "",
 | 
				
			||||||
        service_name: "Новая левая статья",
 | 
					        service_name: hasAnyName ? ruName : "",
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
      articlesStore.articles.en.push({
 | 
					      articlesStore.articles.en.push({
 | 
				
			||||||
        id: newLeftArticleId,
 | 
					        id: newLeftArticleId,
 | 
				
			||||||
        heading: "New Left Article",
 | 
					        heading: hasAnyName ? enName : "",
 | 
				
			||||||
        body: "Fill with content",
 | 
					        body: "",
 | 
				
			||||||
        service_name: "New Left Article",
 | 
					        service_name: hasAnyName ? enName : "",
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
      articlesStore.articles.zh.push({
 | 
					      articlesStore.articles.zh.push({
 | 
				
			||||||
        id: newLeftArticleId,
 | 
					        id: newLeftArticleId,
 | 
				
			||||||
        heading: "新的左侧文章",
 | 
					        heading: hasAnyName ? zhName : "",
 | 
				
			||||||
        body: "填写内容",
 | 
					        body: "",
 | 
				
			||||||
        service_name: "新的左侧文章",
 | 
					        service_name: hasAnyName ? zhName : "",
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
    return newLeftArticleId;
 | 
					    return newLeftArticleId;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -400,16 +400,36 @@ class EditSightStore {
 | 
				
			|||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  createLeftArticle = async () => {
 | 
					  createLeftArticle = async () => {
 | 
				
			||||||
 | 
					    const ruName = (this.sight.ru.name || "").trim();
 | 
				
			||||||
 | 
					    const enName = (this.sight.en.name || "").trim();
 | 
				
			||||||
 | 
					    const zhName = (this.sight.zh.name || "").trim();
 | 
				
			||||||
 | 
					    const hasAnyName = !!(ruName || enName || zhName);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const response = await languageInstance("ru").post(`/article`, {
 | 
					    const response = await languageInstance("ru").post(`/article`, {
 | 
				
			||||||
      heading: "",
 | 
					      heading: hasAnyName ? ruName : "",
 | 
				
			||||||
      body: "",
 | 
					      body: "",
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.sight.common.left_article = response.data.id;
 | 
					    this.sight.common.left_article = response.data.id;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.sight.ru.left.heading = "";
 | 
					    await languageInstance("en").patch(
 | 
				
			||||||
    this.sight.en.left.heading = "";
 | 
					      `/article/${this.sight.common.left_article}`,
 | 
				
			||||||
    this.sight.zh.left.heading = "";
 | 
					      {
 | 
				
			||||||
 | 
					        heading: hasAnyName ? enName : "",
 | 
				
			||||||
 | 
					        body: "",
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    await languageInstance("zh").patch(
 | 
				
			||||||
 | 
					      `/article/${this.sight.common.left_article}`,
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        heading: hasAnyName ? zhName : "",
 | 
				
			||||||
 | 
					        body: "",
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.sight.ru.left.heading = hasAnyName ? ruName : "";
 | 
				
			||||||
 | 
					    this.sight.en.left.heading = hasAnyName ? enName : "";
 | 
				
			||||||
 | 
					    this.sight.zh.left.heading = hasAnyName ? zhName : "";
 | 
				
			||||||
    this.sight.ru.left.body = "";
 | 
					    this.sight.ru.left.body = "";
 | 
				
			||||||
    this.sight.en.left.body = "";
 | 
					    this.sight.en.left.body = "";
 | 
				
			||||||
    this.sight.zh.left.body = "";
 | 
					    this.sight.zh.left.body = "";
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
import React, { useRef, useState, DragEvent, useEffect } from "react";
 | 
					import React, { useRef, DragEvent } from "react";
 | 
				
			||||||
import { Paper, Box, Typography, Button, Tooltip } from "@mui/material";
 | 
					import { Paper, Box, Typography, Button, Tooltip } from "@mui/material";
 | 
				
			||||||
import { X, Info, Plus } from "lucide-react"; // Assuming lucide-react for icons
 | 
					import { X, Info, Plus } from "lucide-react"; // Assuming lucide-react for icons
 | 
				
			||||||
import { editSightStore } from "@shared";
 | 
					import { editSightStore } from "@shared";
 | 
				
			||||||
@@ -27,18 +27,9 @@ export const ImageUploadCard: React.FC<ImageUploadCardProps> = ({
 | 
				
			|||||||
  tooltipText,
 | 
					  tooltipText,
 | 
				
			||||||
}) => {
 | 
					}) => {
 | 
				
			||||||
  const fileInputRef = useRef<HTMLInputElement>(null);
 | 
					  const fileInputRef = useRef<HTMLInputElement>(null);
 | 
				
			||||||
  const [isDragOver, setIsDragOver] = useState(false);
 | 
					 | 
				
			||||||
  const { setFileToUpload } = editSightStore;
 | 
					  const { setFileToUpload } = editSightStore;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  useEffect(() => {
 | 
					 | 
				
			||||||
    if (isDragOver) {
 | 
					 | 
				
			||||||
      console.log("isDragOver");
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }, [isDragOver]);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // --- Click to select file ---
 | 
					 | 
				
			||||||
  const handleZoneClick = () => {
 | 
					  const handleZoneClick = () => {
 | 
				
			||||||
    // Trigger the hidden file input click
 | 
					 | 
				
			||||||
    fileInputRef.current?.click();
 | 
					    fileInputRef.current?.click();
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -68,19 +59,16 @@ export const ImageUploadCard: React.FC<ImageUploadCardProps> = ({
 | 
				
			|||||||
  const handleDragOver = (event: DragEvent<HTMLDivElement>) => {
 | 
					  const handleDragOver = (event: DragEvent<HTMLDivElement>) => {
 | 
				
			||||||
    event.preventDefault(); // Crucial to allow a drop
 | 
					    event.preventDefault(); // Crucial to allow a drop
 | 
				
			||||||
    event.stopPropagation();
 | 
					    event.stopPropagation();
 | 
				
			||||||
    setIsDragOver(true);
 | 
					 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const handleDragLeave = (event: DragEvent<HTMLDivElement>) => {
 | 
					  const handleDragLeave = (event: DragEvent<HTMLDivElement>) => {
 | 
				
			||||||
    event.preventDefault();
 | 
					    event.preventDefault();
 | 
				
			||||||
    event.stopPropagation();
 | 
					    event.stopPropagation();
 | 
				
			||||||
    setIsDragOver(false);
 | 
					 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const handleDrop = async (event: DragEvent<HTMLDivElement>) => {
 | 
					  const handleDrop = async (event: DragEvent<HTMLDivElement>) => {
 | 
				
			||||||
    event.preventDefault(); // Crucial to allow a drop
 | 
					    event.preventDefault(); // Crucial to allow a drop
 | 
				
			||||||
    event.stopPropagation();
 | 
					    event.stopPropagation();
 | 
				
			||||||
    setIsDragOver(false);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const files = event.dataTransfer.files;
 | 
					    const files = event.dataTransfer.files;
 | 
				
			||||||
    if (files && files.length > 0) {
 | 
					    if (files && files.length > 0) {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -38,7 +38,7 @@ export function MediaViewer({
 | 
				
			|||||||
      // Используем новый cache manager для очистки кеша
 | 
					      // Используем новый cache manager для очистки кеша
 | 
				
			||||||
      clearMediaTransitionCache(
 | 
					      clearMediaTransitionCache(
 | 
				
			||||||
        previousMediaId,
 | 
					        previousMediaId,
 | 
				
			||||||
        media?.id || null,
 | 
					
 | 
				
			||||||
        media?.media_type
 | 
					        media?.media_type
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
		Reference in New Issue
	
	Block a user