diff --git a/.env b/.env index 6517129..59c6180 100644 --- a/.env +++ b/.env @@ -1 +1,2 @@ -VITE_REACT_APP = 'https://wn.krbl.ru/' \ No newline at end of file +VITE_REACT_APP ='https://wn.krbl.ru/' +VITE_KRBL_MEDIA='https://wn.krbl.ru/media/' \ No newline at end of file diff --git a/package.json b/package.json index 0a7ad51..54bdc7f 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,9 @@ "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.0", "@mui/material": "^7.1.0", + "@photo-sphere-viewer/core": "^5.13.2", + "@react-three/drei": "^10.1.2", + "@react-three/fiber": "^9.1.2", "@tailwindcss/vite": "^4.1.8", "axios": "^1.9.0", "easymde": "^2.20.0", @@ -23,11 +26,13 @@ "react": "^19.1.0", "react-dom": "^19.1.0", "react-markdown": "^10.1.0", + "react-photo-sphere-viewer": "^6.2.3", "react-router-dom": "^7.6.1", "react-simplemde-editor": "^5.2.0", "react-toastify": "^11.0.5", "rehype-raw": "^7.0.0", - "tailwindcss": "^4.1.8" + "tailwindcss": "^4.1.8", + "three": "^0.177.0" }, "devDependencies": { "@eslint/js": "^9.25.0", diff --git a/src/app/index.tsx b/src/app/index.tsx index b435b31..1a152cd 100644 --- a/src/app/index.tsx +++ b/src/app/index.tsx @@ -3,12 +3,12 @@ import * as React from "react"; import { BrowserRouter } from "react-router-dom"; import { Router } from "./router"; -import { theme } from "@shared"; +import { CustomTheme } from "@shared"; import { ThemeProvider } from "@mui/material/styles"; import { ToastContainer } from "react-toastify"; export const App: React.FC = () => ( - + diff --git a/src/app/router/index.tsx b/src/app/router/index.tsx index 5889955..8eabdb9 100644 --- a/src/app/router/index.tsx +++ b/src/app/router/index.tsx @@ -6,8 +6,9 @@ import { MainPage, SightPage, } from "@pages"; -import { authStore } from "@shared"; +import { authStore, editSightStore, sightsStore } from "@shared"; import { Layout } from "@widgets"; +import { useEffect } from "react"; import { Navigate, Outlet, Route, Routes, useLocation } from "react-router-dom"; @@ -32,6 +33,11 @@ const ProtectedRoute = ({ children }: { children: React.ReactNode }) => { }; export const Router = () => { + const pathname = useLocation(); + useEffect(() => { + editSightStore.clearSightInfo(); + sightsStore.clearCreateSight(); + }, [pathname]); return ( { const [value, setValue] = useState(0); const { sight, getSight } = sightsStore; - const { getArticles } = articlesStore; + const { articles, getArticles } = articlesStore; const { language } = languageStore; const { id } = useParams(); @@ -28,53 +28,54 @@ export const EditSightPage = observer(() => { const fetchData = async () => { if (id) { await getSight(Number(id)); - await getArticles(); + await getArticles(language); } }; fetchData(); }, [id, language]); return ( - + articles && + sight && ( - - - - - - + + + + + + - {sight && (
- - + +
- )} - + + ) ); }); diff --git a/src/shared/const/index.ts b/src/shared/const/index.ts index 6903264..28586ba 100644 --- a/src/shared/const/index.ts +++ b/src/shared/const/index.ts @@ -1 +1,9 @@ export const API_URL = "https://wn.krbl.ru"; +export const MEDIA_TYPE_LABELS = { + 1: "Фото", + 2: "Видео", + 3: "Иконка", + 4: "Водяной знак", + 5: "Панорама", + 6: "3Д-модель", +}; diff --git a/src/shared/lib/mui/theme.ts b/src/shared/lib/mui/theme.ts index fb1b266..185133c 100644 --- a/src/shared/lib/mui/theme.ts +++ b/src/shared/lib/mui/theme.ts @@ -1,17 +1,45 @@ import { createTheme } from "@mui/material/styles"; -export const theme = createTheme({ - // You can customize your theme here +export const COLORS = { + primary: "#7f6b58", + secondary: "#48989f", +}; + +const theme = { palette: { - mode: "light", + primary: { + main: COLORS.primary, + }, + secondary: { + main: COLORS.secondary, + }, }, components: { - MuiDrawer: { + MuiAppBar: { styleOverrides: { - paper: { - backgroundColor: "#fff", + root: { + backgroundColor: COLORS.secondary, }, }, }, }, -}); +}; + +export const CustomTheme = { + Light: createTheme({ + palette: { + ...theme.palette, + }, + components: { + ...theme.components, + }, + }), + Dark: createTheme({ + palette: { + ...theme.palette, + }, + components: { + ...theme.components, + }, + }), +}; diff --git a/src/shared/modals/PreviewMediaDialog/index.tsx b/src/shared/modals/PreviewMediaDialog/index.tsx new file mode 100644 index 0000000..43d6a15 --- /dev/null +++ b/src/shared/modals/PreviewMediaDialog/index.tsx @@ -0,0 +1,229 @@ +import { + articlesStore, + authStore, + Language, + mediaStore, + MEDIA_TYPE_LABELS, + API_URL, +} from "@shared"; +import { observer } from "mobx-react-lite"; +import { useEffect, useRef, useState, useCallback } from "react"; +import { + Dialog, + DialogTitle, + DialogContent, + DialogActions, + Button, + TextField, + Paper, + Box, + Typography, + CircularProgress, + Alert, + Snackbar, +} from "@mui/material"; +import { Download, Save } from "lucide-react"; +import { ReactMarkdownComponent, MediaViewer } from "@widgets"; +import { authInstance } from "@shared"; + +interface PreviewMediaDialogProps { + open: boolean; + onClose: () => void; + mediaId: string; +} + +export const PreviewMediaDialog = observer( + ({ open, onClose, mediaId }: PreviewMediaDialogProps) => { + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + const [success, setSuccess] = useState(false); + const media = mediaId + ? mediaStore.media.find((m) => m.id === mediaId) + : null; + const [mediaName, setMediaName] = useState(media?.media_name ?? ""); + const [mediaFilename, setMediaFilename] = useState(media?.filename ?? ""); + + // Reset form when media changes + useEffect(() => { + if (media) { + setMediaName(media.media_name); + setMediaFilename(media.filename); + } + }, [media]); + + useEffect(() => { + const handleKeyPress = (event: KeyboardEvent) => { + if (event.key.toLowerCase() === "enter" && !event.ctrlKey) { + event.preventDefault(); + onClose(); + } + }; + + window.addEventListener("keydown", handleKeyPress); + return () => window.removeEventListener("keydown", handleKeyPress); + }, [onClose]); + + const handleSave = async () => { + if (!mediaId) return; + + setIsLoading(true); + setError(null); + + try { + await authInstance.patch(`/media/${mediaId}`, { + media_name: mediaName, + filename: mediaFilename, + type: media?.media_type, + }); + + // Update local store + await mediaStore.getMedia(); + setSuccess(true); + } catch (err) { + setError(err instanceof Error ? err.message : "Failed to save media"); + } finally { + setIsLoading(false); + } + }; + + const handleClose = () => { + setError(null); + setSuccess(false); + onClose(); + }; + + if (!media) { + return null; + } + + return ( + <> + + Просмотр медиа + + + + setMediaName(e.target.value)} + label="Название медиа" + disabled={isLoading} + /> + setMediaFilename(e.target.value)} + label="Название файла" + disabled={isLoading} + /> + + + + + + + + + + + + + + + + + + + + + + setError(null)} + > + setError(null)}> + {error} + + + + setSuccess(false)} + > + setSuccess(false)}> + Медиа успешно сохранено + + + + ); + } +); diff --git a/src/shared/modals/SelectArticleDialog/index.tsx b/src/shared/modals/SelectArticleDialog/index.tsx index e59b718..cf27742 100644 --- a/src/shared/modals/SelectArticleDialog/index.tsx +++ b/src/shared/modals/SelectArticleDialog/index.tsx @@ -1,6 +1,6 @@ -import { articlesStore } from "@shared"; +import { articlesStore, authStore, Language } from "@shared"; import { observer } from "mobx-react-lite"; -import { useEffect, useRef, useState } from "react"; +import { useEffect, useState } from "react"; import { Dialog, DialogTitle, @@ -31,38 +31,56 @@ export const SelectArticleModal = observer( open, onClose, onSelectArticle, - - linkedArticleIds = [], // Default to empty array if not provided + linkedArticleIds = [], }: SelectArticleModalProps) => { - const { articles, getArticle } = articlesStore; // articles here refers to fetched articles from store + const { articles, getArticle, getArticleMedia } = articlesStore; const [searchQuery, setSearchQuery] = useState(""); - const [hoveredArticleId, setHoveredArticleId] = useState( + const [selectedArticleId, setSelectedArticleId] = useState( null ); - const hoverTimerRef = useRef(null); + const [isLoading, setIsLoading] = useState(false); + + // Reset selection when modal opens/closes + useEffect(() => { + if (open) { + setSelectedArticleId(null); + articlesStore.articleData = null; + articlesStore.articleMedia = null; + } + }, [open]); useEffect(() => { - if (hoveredArticleId) { - hoverTimerRef.current = setTimeout(() => { - getArticle(hoveredArticleId); - }, 200); - } - - return () => { - if (hoverTimerRef.current) { - clearTimeout(hoverTimerRef.current); + const handleKeyPress = (event: KeyboardEvent) => { + if (event.key.toLowerCase() === "enter") { + event.preventDefault(); + if (selectedArticleId) { + onSelectArticle(selectedArticleId); + onClose(); + } } }; - }, [hoveredArticleId, getArticle]); - const handleArticleHover = (articleId: string) => { - setHoveredArticleId(articleId); - }; + window.addEventListener("keydown", handleKeyPress); + return () => { + window.removeEventListener("keydown", handleKeyPress); + }; + }, [selectedArticleId, onSelectArticle, onClose]); - const handleArticleLeave = () => { - setHoveredArticleId(null); - if (hoverTimerRef.current) { - clearTimeout(hoverTimerRef.current); + const handleArticleClick = async (articleId: string) => { + if (selectedArticleId === articleId) return; + + setSelectedArticleId(articleId); + setIsLoading(true); + + try { + await Promise.all([getArticle(articleId), getArticleMedia(articleId)]); + } catch (error) { + console.error("Failed to fetch article data:", error); + // Reset article data on error + articlesStore.articleData = null; + articlesStore.articleMedia = null; + } finally { + setIsLoading(false); } }; @@ -72,21 +90,38 @@ export const SelectArticleModal = observer( article.service_name.toLowerCase().includes(searchQuery.toLowerCase()) ); + const token = localStorage.getItem("token"); return ( - + Выберите существующую статью - + setSearchQuery(e.target.value)} - sx={{ mb: 2, mt: 1 }} + sx={{ mb: 2, mt: 1, px: 2 }} InputProps={{ startAdornment: ( @@ -95,27 +130,51 @@ export const SelectArticleModal = observer( ), }} /> - - {filteredArticles.map((article) => ( - onSelectArticle(article.id)} - onMouseEnter={() => handleArticleHover(article.id)} - onMouseLeave={handleArticleLeave} - sx={{ - borderRadius: 1, - mb: 0.5, - "&:hover": { - backgroundColor: "action.hover", - }, - }} + + {filteredArticles.length === 0 ? ( + - - - ))} + {searchQuery ? "Статьи не найдены" : "Нет доступных статей"} + + ) : ( + filteredArticles.map((article) => ( + handleArticleClick(article.id)} + onDoubleClick={() => onSelectArticle(article.id)} + selected={selectedArticleId === article.id} + disabled={isLoading} + sx={{ + borderRadius: 1, + mb: 0.5, + "&:hover": { + backgroundColor: "action.hover", + }, + "&.Mui-selected": { + backgroundColor: "primary.main", + color: "primary.contrastText", + "&:hover": { + backgroundColor: "primary.dark", + }, + }, + }} + > + + + )) + )} - + - {/* Media Preview Area */} - - - + {isLoading ? ( + + Загрузка... + + ) : ( + <> + {articlesStore.articleMedia && ( + + {articlesStore.articleMedia.filename} + + )} + {!articlesStore.articleMedia && ( + + + + )} - {/* Title Area */} - - - {articlesStore.articleData?.heading || - "Нет данных для предпросмотра"} - - + + + {articlesStore.articleData?.heading || "Выберите статью"} + + - {/* Body Preview Area */} - - - + + {articlesStore.articleData?.body ? ( + + ) : ( + + Предпросмотр статьи появится здесь + + )} + + + )} - + + ); diff --git a/src/shared/modals/SelectMediaDialog/index.tsx b/src/shared/modals/SelectMediaDialog/index.tsx new file mode 100644 index 0000000..c5676c6 --- /dev/null +++ b/src/shared/modals/SelectMediaDialog/index.tsx @@ -0,0 +1,178 @@ +import { articlesStore, authStore, Language, mediaStore } from "@shared"; +import { observer } from "mobx-react-lite"; +import { useEffect, useRef, useState } from "react"; +import { + Dialog, + DialogTitle, + DialogContent, + DialogActions, + Button, + TextField, + List, + ListItemButton, + ListItemText, + Paper, + Box, + Typography, + InputAdornment, +} from "@mui/material"; +import { ImagePlus, Search } from "lucide-react"; +import { ReactMarkdownComponent, MediaViewer } from "@widgets"; + +interface SelectMediaDialogProps { + open: boolean; // Corrected prop name + onClose: () => void; + onSelectMedia: (mediaId: string) => void; // Renamed from onSelectArticle + linkedMediaIds?: string[]; // Renamed from linkedArticleIds, assuming it refers to media already in use +} + +export const SelectMediaDialog = observer( + ({ + open, // Corrected prop name + onClose, + onSelectMedia, // Renamed prop + linkedMediaIds = [], // Default to empty array if not provided, renamed + }: SelectMediaDialogProps) => { + const { media, getMedia } = mediaStore; // Confirmed: using mediaStore for media + const [searchQuery, setSearchQuery] = useState(""); + const [hoveredMediaId, setHoveredMediaId] = useState(null); + const hoverTimerRef = useRef(null); + + // Fetch media on component mount + useEffect(() => { + getMedia(); + }, [getMedia]); // getMedia should be a dependency to avoid lint warnings if it's not stable + + // Keyboard event listener for "Enter" key to select hovered media + useEffect(() => { + const handleKeyPress = (event: KeyboardEvent) => { + if (event.key === "Enter") { + event.preventDefault(); // Prevent browser default action (e.g., form submission) + + if (hoveredMediaId) { + onSelectMedia(hoveredMediaId); // Call onSelectMedia + onClose(); + } + } + }; + + window.addEventListener("keydown", handleKeyPress); + return () => { + window.removeEventListener("keydown", handleKeyPress); + }; + }, [hoveredMediaId, onSelectMedia, onClose]); // Dependencies for keyboard listener + + // Effect for handling hover timeout (if you want to clear the preview after a delay) + // Based on the original code, it seemed like you wanted a delay for showing, + // but typically for a preview, it's immediate on hover and cleared on mouse leave. + // I've removed the 5-second timeout for setting the ID as it's counter-intuitive for a live preview. + // If you intend for the preview to disappear after a short while *after* the mouse leaves, + // you would implement a mouseleave timer. For now, it will clear on mouseleave. + + const handleMouseEnter = (mediaId: string) => { + if (hoverTimerRef.current) { + clearTimeout(hoverTimerRef.current); + } + setHoveredMediaId(mediaId); + }; + + const handleMouseLeave = () => { + // You can add a small delay here if you want the preview to linger for a moment + // before disappearing, e.g., setTimeout(() => setHoveredMediaId(null), 200); + setHoveredMediaId(null); + }; + + const filteredMedia = media + .filter((mediaItem) => !linkedMediaIds.includes(mediaItem.id)) // Use mediaItem to avoid name collision + .filter((mediaItem) => + mediaItem.media_name.toLowerCase().includes(searchQuery.toLowerCase()) + ); + + // Find the currently hovered media object for MediaViewer + const currentHoveredMedia = hoveredMediaId + ? media.find((m) => m.id === hoveredMediaId) + : null; + + return ( + + Выберите существующее медиа + + + {" "} + {/* Added padding for consistency */} + setSearchQuery(e.target.value)} + sx={{ mb: 2, mt: 1 }} + InputProps={{ + startAdornment: ( + + + + ), + }} + /> + + {filteredMedia.length > 0 ? ( + filteredMedia.map( + ( + mediaItem // Use mediaItem to avoid confusion + ) => ( + onSelectMedia(mediaItem.id)} // Call onSelectMedia + onMouseEnter={() => handleMouseEnter(mediaItem.id)} + onMouseLeave={handleMouseLeave} + sx={{ + borderRadius: 1, + mb: 0.5, + "&:hover": { + backgroundColor: "action.hover", + }, + }} + > + + + ) + ) + ) : ( + + Медиа не найдено или все медиа уже прикреплены. + + )} + + + {currentHoveredMedia ? ( // Only render MediaViewer if currentHoveredMedia is found + + + + ) : ( + + + Наведите на медиа в списке для предпросмотра. + + + )} + + + + + + ); + } +); diff --git a/src/shared/modals/index.ts b/src/shared/modals/index.ts index e714367..22df340 100644 --- a/src/shared/modals/index.ts +++ b/src/shared/modals/index.ts @@ -1 +1,3 @@ export * from "./SelectArticleDialog"; +export * from "./SelectMediaDialog"; +export * from "./PreviewMediaDialog"; diff --git a/src/shared/store/ArticlesStore/index.tsx b/src/shared/store/ArticlesStore/index.tsx index 600eb8f..5048469 100644 --- a/src/shared/store/ArticlesStore/index.tsx +++ b/src/shared/store/ArticlesStore/index.tsx @@ -1,36 +1,78 @@ -import { authInstance } from "@shared"; -import { makeAutoObservable, runInAction } from "mobx"; +import { authInstance, editSightStore, Language, languageStore } from "@shared"; +import { computed, makeAutoObservable, runInAction } from "mobx"; export type Article = { - id: string; + id: number; heading: string; body: string; service_name: string; }; +type Media = { + id: string; + filename: string; + media_name: string; + media_type: number; +}; + class ArticlesStore { constructor() { makeAutoObservable(this); } - articles: Article[] = []; + articles: { [key in Language]: Article[] } = { + ru: [], + en: [], + zh: [], + }; articleData: Article | null = null; + articleMedia: Media | null = null; + articleLoading: boolean = false; - getArticles = async () => { + getArticles = async (language: Language) => { + this.articleLoading = true; const response = await authInstance.get("/article"); runInAction(() => { - this.articles = response.data; + this.articles[language] = response.data; }); + this.articleLoading = false; }; - getArticle = async (id: string) => { + getArticle = async (id: number) => { + this.articleLoading = true; const response = await authInstance.get(`/article/${id}`); runInAction(() => { this.articleData = response.data; }); + this.articleLoading = false; }; + + getSightArticles = async (id: number) => { + const response = await authInstance.get(`/sight/${id}/article`); + + runInAction(() => { + editSightStore.sightInfo[languageStore.language].right = response.data; + }); + }; + + getArticleMedia = async (id: number) => { + const response = await authInstance.get(`/article/${id}/media`); + + runInAction(() => { + this.articleMedia = response.data[0]; + }); + }; + + getArticleByArticleId = computed(() => { + if (editSightStore.sightInfo.left_article) { + return this.articles[languageStore.language].find( + (a) => a.id == editSightStore.sightInfo.left_article + ); + } + return null; + }); } export const articlesStore = new ArticlesStore(); diff --git a/src/shared/store/EditSightStore/index.tsx b/src/shared/store/EditSightStore/index.tsx new file mode 100644 index 0000000..8faf2d9 --- /dev/null +++ b/src/shared/store/EditSightStore/index.tsx @@ -0,0 +1,151 @@ +// @shared/stores/editSightStore.ts +import { Language } from "@shared"; +import { makeAutoObservable } from "mobx"; + +export interface MediaObject { + id: string; + filename: string; + media_type: number; +} + +type SightBaseInfo = { + id: number; + city_id: number; + city: string; + latitude: number; + longitude: number; + thumbnail: string; + watermark_lu: string; + watermark_rd: string; + left_article: number; + preview_media: string; + video_preview: string; +}; + +export interface RightArticleBlock { + id: string; + type: "article" | "preview_media"; + name: string; + linkedArticleId?: string; + heading: string; + body: string; + media: MediaObject | null; +} + +type SightInfo = SightBaseInfo & { + [key in Language]: { + info: { + name: string; + address: string; + }; + left: { + loaded: boolean; // Означает, что данные для этого языка были инициализированы/загружены + heading: string; + body: string; + media: MediaObject | null; + }; + right: RightArticleBlock[]; + }; +}; + +class EditSightStore { + sightInfo: SightInfo = { + id: 0, + city_id: 0, + city: "", + latitude: 0, + longitude: 0, + thumbnail: "", + watermark_lu: "", + watermark_rd: "", + left_article: 0, + preview_media: "", + video_preview: "", + ru: { + info: { name: "", address: "" }, + left: { loaded: false, heading: "", body: "", media: null }, + right: [], + }, + en: { + info: { name: "", address: "" }, + left: { loaded: false, heading: "", body: "", media: null }, + right: [], + }, + zh: { + info: { name: "", address: "" }, + left: { loaded: false, heading: "", body: "", media: null }, + right: [], + }, + }; + + constructor() { + makeAutoObservable(this); + } + + // loadSightInfo: Используется для первоначальной загрузки данных для ЯЗЫКА. + // Она устанавливает loaded: true, чтобы в будущем не перезатирать данные. + loadSightInfo = ( + language: Language, + heading: string, + body: string, + media: MediaObject | null + ) => { + // Важно: если данные уже были загружены или изменены, не перезаписывайте их. + // Это предотвращает потерю пользовательского ввода при переключении языков. + // Если хотите принудительную загрузку, добавьте другой метод или параметр. + if (!this.sightInfo[language].left.loaded) { + // <--- Только если еще не загружено + this.sightInfo[language].left.heading = heading; + this.sightInfo[language].left.body = body; + this.sightInfo[language].left.media = media; + this.sightInfo[language].left.loaded = true; // <--- Устанавливаем loaded только при загрузке + } + }; + + // updateSightInfo: Используется для сохранения ЛЮБЫХ пользовательских изменений. + // Она НЕ должна влиять на флаг 'loaded', который управляется 'loadSightInfo'. + updateSightInfo = ( + language: Language, + heading: string, + body: string, + media: MediaObject | null + ) => { + this.sightInfo[language].left.heading = heading; + this.sightInfo[language].left.body = body; + this.sightInfo[language].left.media = media; + // this.sightInfo[language].left.loaded = true; // <-- УДАЛИТЕ эту строку + }; + + clearSightInfo = () => { + this.sightInfo = { + id: 0, + city_id: 0, + city: "", + latitude: 0, + longitude: 0, + thumbnail: "", + watermark_lu: "", + watermark_rd: "", + left_article: 0, + preview_media: "", + video_preview: "", + ru: { + info: { name: "", address: "" }, + left: { loaded: false, heading: "", body: "", media: null }, + right: [], + }, + en: { + info: { name: "", address: "" }, + left: { loaded: false, heading: "", body: "", media: null }, + right: [], + }, + zh: { + info: { name: "", address: "" }, + left: { loaded: false, heading: "", body: "", media: null }, + right: [], + }, + }; + }; +} + +export const editSightStore = new EditSightStore(); diff --git a/src/shared/store/MediaStore/index.tsx b/src/shared/store/MediaStore/index.tsx new file mode 100644 index 0000000..c06f8b6 --- /dev/null +++ b/src/shared/store/MediaStore/index.tsx @@ -0,0 +1,27 @@ +import { makeAutoObservable, runInAction } from "mobx"; +import { authInstance } from "@shared"; + +type Media = { + id: string; + filename: string; + media_name: string; + media_type: number; +}; + +class MediaStore { + media: Media[] = []; + + constructor() { + makeAutoObservable(this); + } + + getMedia = async () => { + const response = await authInstance.get("/media"); + + runInAction(() => { + this.media = [...response.data]; + }); + }; +} + +export const mediaStore = new MediaStore(); diff --git a/src/shared/store/SightsStore/index.tsx b/src/shared/store/SightsStore/index.tsx index 7acd0d6..5df9823 100644 --- a/src/shared/store/SightsStore/index.tsx +++ b/src/shared/store/SightsStore/index.tsx @@ -1,12 +1,17 @@ -import { authInstance, languageInstance, languageStore } from "@shared"; -import { makeAutoObservable, runInAction } from "mobx"; +import { + articlesStore, + authInstance, + languageInstance, + languageStore, + editSightStore, +} from "@shared"; +import { computed, makeAutoObservable, runInAction } from "mobx"; export type Language = "ru" | "en" | "zh"; export type MultilingualContent = { [key in Language]: { name: string; - description: string; address: string; }; }; @@ -30,7 +35,6 @@ export type Sight = { export type CreateSight = { [key in Language]: { name: string; - description: string; address: string; }; }; @@ -39,9 +43,9 @@ class SightsStore { sights: Sight[] = []; sight: Sight | null = null; createSight: CreateSight = { - ru: { name: "", description: "", address: "" }, - en: { name: "", description: "", address: "" }, - zh: { name: "", description: "", address: "" }, + ru: { name: "", address: "" }, + en: { name: "", address: "" }, + zh: { name: "", address: "" }, }; constructor() { @@ -60,6 +64,41 @@ class SightsStore { runInAction(() => { this.sight = response.data; + editSightStore.sightInfo = { + ...editSightStore.sightInfo, + id: response.data.id, + city_id: response.data.city_id, + city: response.data.city, + latitude: response.data.latitude, + longitude: response.data.longitude, + thumbnail: response.data.thumbnail, + watermark_lu: response.data.watermark_lu, + watermark_rd: response.data.watermark_rd, + left_article: response.data.left_article, + preview_media: response.data.preview_media, + video_preview: response.data.video_preview, + [languageStore.language]: { + info: { + name: response.data.name, + address: response.data.address, + description: response.data.description, + }, + left: { + heading: editSightStore.sightInfo[languageStore.language].left + .loaded + ? editSightStore.sightInfo[languageStore.language].left.heading + : articlesStore.articles[languageStore.language].find( + (article) => article.id === response.data.left_article + )?.heading, + body: editSightStore.sightInfo[languageStore.language].left.loaded + ? editSightStore.sightInfo[languageStore.language].left.body + : articlesStore.articles[languageStore.language].find( + (article) => article.id === response.data.left_article + )?.body, + }, + }, + }; + console.log(editSightStore.sightInfo); }); }; @@ -70,7 +109,6 @@ class SightsStore { const id = ( await authInstance.post("/sight", { name: this.createSight[languageStore.language].name, - description: this.createSight[languageStore.language].description, address: this.createSight[languageStore.language].address, city_id: city, latitude: coordinates.latitude, @@ -86,8 +124,6 @@ class SightsStore { `/sight/${id}`, { name: this.createSight[anotherLanguages[0] as Language].name, - description: - this.createSight[anotherLanguages[0] as Language].description, address: this.createSight[anotherLanguages[0] as Language].address, city_id: city, latitude: coordinates.latitude, @@ -99,8 +135,6 @@ class SightsStore { `/sight/${id}`, { name: this.createSight[anotherLanguages[1] as Language].name, - description: - this.createSight[anotherLanguages[1] as Language].description, address: this.createSight[anotherLanguages[1] as Language].address, city_id: city, latitude: coordinates.latitude, @@ -110,9 +144,9 @@ class SightsStore { runInAction(() => { this.createSight = { - ru: { name: "", description: "", address: "" }, - en: { name: "", description: "", address: "" }, - zh: { name: "", description: "", address: "" }, + ru: { name: "", address: "" }, + en: { name: "", address: "" }, + zh: { name: "", address: "" }, }; }); }; @@ -139,22 +173,41 @@ class SightsStore { this.createSight = { ru: { name: "", - description: "", address: "", }, en: { name: "", - description: "", address: "", }, zh: { name: "", - description: "", address: "", }, }; }); }; + + sightData = computed(() => { + return { + name: this.sight?.name, + address: this.sight?.address, + city_id: this.sight?.city_id, + latitude: this.sight?.latitude, + longitude: this.sight?.longitude, + thumbnail: this.sight?.thumbnail, + watermark_lu: this.sight?.watermark_lu, + watermark_rd: this.sight?.watermark_rd, + left_article: this.sight?.left_article, + preview_media: this.sight?.preview_media, + video_preview: this.sight?.video_preview, + [languageStore.language]: { + info: { + name: this.sight?.name, + address: this.sight?.address, + }, + }, + }; + }); } export const sightsStore = new SightsStore(); diff --git a/src/shared/store/index.ts b/src/shared/store/index.ts index 9be9b82..cb82e5c 100644 --- a/src/shared/store/index.ts +++ b/src/shared/store/index.ts @@ -6,3 +6,5 @@ export * from "./SnapshotStore"; export * from "./SightsStore"; export * from "./CityStore"; export * from "./ArticlesStore"; +export * from "./EditSightStore"; +export * from "./MediaStore"; diff --git a/src/shared/ui/CoordinatesInput/index.tsx b/src/shared/ui/CoordinatesInput/index.tsx index 60918af..b34361b 100644 --- a/src/shared/ui/CoordinatesInput/index.tsx +++ b/src/shared/ui/CoordinatesInput/index.tsx @@ -2,11 +2,15 @@ import { Box, TextField } from "@mui/material"; import { useEffect, useState } from "react"; export const CoordinatesInput = ({ + initialValue, setValue, }: { + initialValue: { latitude: number; longitude: number }; setValue: (value: { latitude: number; longitude: number }) => void; }) => { - const [inputValue, setInputValue] = useState(""); + const [inputValue, setInputValue] = useState( + `${initialValue.latitude} ${initialValue.longitude}` + ); useEffect(() => { setValue({ diff --git a/src/widgets/LanguageSwitcher/index.tsx b/src/widgets/LanguageSwitcher/index.tsx index 0d34162..b6e1650 100644 --- a/src/widgets/LanguageSwitcher/index.tsx +++ b/src/widgets/LanguageSwitcher/index.tsx @@ -45,7 +45,7 @@ export const LanguageSwitcher = observer(() => { }; return ( -
+
{/* Added some styling for better visualization */} {LANGUAGES.map((lang) => ( - - - - + + Логотип + + + { + setIsMediaModalOpen(true); + }} > - Водяной знак (л.в) - - - - - - { + setIsPreviewMediaOpen(true); + setMediaId(editSightStore.sightInfo?.thumbnail); + }} + /> + ) : ( + + )} + + + + - mockSightData.watermark_lu && - handleSelectMedia("watermark_lu") - } > - {mockSightData.watermark_lu ? ( - Знак л.в - ) : ( - - )} - - - + + + Водяной знак (л.в) + + + + + + { + setIsPreviewMediaOpen(true); + setMediaId(editSightStore.sightInfo?.watermark_lu); + }} + > + {editSightStore.sightInfo?.watermark_lu ? ( + Знак л.в { + setIsMediaModalOpen(true); + setMediaId(editSightStore.sightInfo?.watermark_lu); + }} + /> + ) : ( + + )} + + + - - - - Водяной знак (п.в) - - - - - - - mockSightData.watermark_rd && - handleSelectMedia("watermark_rd") - } > - {mockSightData.watermark_rd ? ( - Знак п.в - ) : ( - - )} - - - + + + Водяной знак (п.в) + + + + + + editSightStore.sightInfo?.watermark_rd} + > + {editSightStore.sightInfo?.watermark_rd ? ( + Знак п.в { + setIsPreviewMediaOpen(true); + setMediaId(editSightStore.sightInfo?.watermark_rd); + }} + /> + ) : ( + + )} + + + + - */} {/* LanguageSwitcher positioned at the top right */} @@ -384,8 +437,41 @@ export const InformationTab = observer( - - + + + {/* Media Menu */} + + Создать новую + Выбрать существующую + + + { + setIsAddMediaOpen(false); + setActiveMenuType(null); + }} + onSelectArticle={handleMediaSelect} + /> + + setIsPreviewMediaOpen(false)} + mediaId={mediaId} + /> + ); } ); diff --git a/src/widgets/SightTabs/LeftWidgetTab/index.tsx b/src/widgets/SightTabs/LeftWidgetTab/index.tsx index 98d1742..ca6c673 100644 --- a/src/widgets/SightTabs/LeftWidgetTab/index.tsx +++ b/src/widgets/SightTabs/LeftWidgetTab/index.tsx @@ -1,242 +1,348 @@ +// @widgets/LeftWidgetTab.tsx import { Box, Button, TextField, Paper, Typography } from "@mui/material"; -import { BackButton, Sight, TabPanel } from "@shared"; -import { ReactMarkdownComponent, ReactMarkdownEditor } from "@widgets"; +import { + articlesStore, + BackButton, + TabPanel, + languageStore, + SelectMediaDialog, + editSightStore, +} from "@shared"; +import { + LanguageSwitcher, + ReactMarkdownComponent, + ReactMarkdownEditor, +} from "@widgets"; import { Unlink, Trash2, ImagePlus } from "lucide-react"; -import { useState } from "react"; +import { useState, useEffect, useCallback } from "react"; +import { observer } from "mobx-react-lite"; -export const LeftWidgetTab = ({ - value, - index, - data, -}: { - value: number; - index: number; - data?: Sight; -}) => { - const [articleTitle, setArticleTitle] = useState(""); - const [markdownContent, setMarkdownContent] = useState(""); - const [articleMedia, setArticleMedia] = useState(null); // Для превью медиа +export const LeftWidgetTab = observer( + ({ value, index }: { value: number; index: number }) => { + const { sightInfo, updateSightInfo, loadSightInfo } = editSightStore; + const { articleLoading, getArticleByArticleId } = articlesStore; + const { language } = languageStore; + const linkedArticle = getArticleByArticleId.get(); // Получаем связанную статью + const data = sightInfo[languageStore.language]; // Получаем данные для текущего языка - const handleSelectMediaForArticle = () => { - // Логика открытия модального окна для выбора медиа для статьи - console.log("Select media fo r left article"); - // Для примера, установим моковое изображение - // setArticleMedia("https://via.placeholder.com/350x200.png?text=Article+Media"); - }; + useEffect(() => { + // Этот useEffect должен загружать данные ИЗ СВЯЗАННОЙ СТАТЬИ + // ТОЛЬКО ЕСЛИ данные для ТЕКУЩЕГО ЯЗЫКА еще не были загружены + // или если sightInfo.left_article изменился (т.е. привязали новую статью). - const handleUnlinkArticle = () => { - console.log("Unlink left article"); - }; + // Мы также должны учитывать, что linkedArticle может измениться (т.е. новую статью привязали) + // или language изменился. + // Если для текущего языка данные еще не "загружены" (`loaded: false`), + // тогда мы берем их из `linkedArticle` и инициализируем. + console.log("data.left.loaded", data.left.loaded); + if (!data.left.loaded) { + // <--- КЛЮЧЕВОЕ УСЛОВИЕ + if (linkedArticle && !articleLoading) { + console.log("loadSightInfo", linkedArticle, language); + loadSightInfo( + languageStore.language, + linkedArticle.heading, + linkedArticle.body || "", + null + ); + } + } + // Зависимости: linkedArticle (для реакции на изменение привязанной статьи), + // languageStore.language (для реакции на изменение языка), + // loadSightInfo (чтобы useEffect знал об изменениях в функции), + // data.left.loaded (чтобы useEffect перепроверил условие, когда этот флаг изменится). + // Важно: если data.left.loaded становится true, то этот эффект не будет + // перезапускаться для того же языка. + }, [ + linkedArticle?.heading, + language, + loadSightInfo, + data.left.loaded, + articleLoading, + ]); - const handleDeleteArticle = () => { - console.log("Delete left article"); - }; + const [isSelectMediaDialogOpen, setIsSelectMediaDialogOpen] = + useState(false); - const handleSave = () => { - console.log("Saving left widget..."); - }; + const handleOpenMediaDialog = useCallback(() => { + setIsSelectMediaDialogOpen(true); + }, []); - return ( - - - + const handleMediaSelected = useCallback( + (selectedMedia: any) => { + // При выборе медиа, обновляем данные для ТЕКУЩЕГО ЯЗЫКА + // сохраняя текущие heading и body. + updateSightInfo( + languageStore.language, + data.left.heading, + data.left.body, + selectedMedia + ); + setIsSelectMediaDialogOpen(false); + }, + [ + languageStore.language, + data.left.heading, + data.left.body, + updateSightInfo, + ] + ); - { + setIsSelectMediaDialogOpen(false); + }, []); + + // ... (остальной JSX код остался почти без изменений) + return ( + + + - Левая статья - - - - - + - - {/* Левая колонка: Редактирование */} - - setArticleTitle(e.target.value)} - variant="outlined" - sx={{ width: "100%" }} // Примерная ширина как на макете - /> - - - - {/* Блок МЕДИА для статьи */} - - - МЕДИА - - {/* Здесь будет UI для управления медиа статьи */} - {articleMedia ? ( - - Article media - - ) : ( - Левая статья + + {linkedArticle && ( + )} - - - + + - {/* Правая колонка: Предпросмотр */} - - Предпросмотр - + {/* Левая колонка: Редактирование */} + + + updateSightInfo( + languageStore.language, + e.target.value, + data.left.body, + data.left.media + ) + } + variant="outlined" + fullWidth + /> + + + updateSightInfo( + languageStore.language, + data.left.heading, + value, + data.left.media + ) + } + /> + + {/* Блок МЕДИА для статьи */} + + + МЕДИА + + {data.left.media ? ( + + Selected media + + ) : ( + + Нет медиа + + )} + + {data.left.media && ( + + )} + + + + {/* Правая колонка: Предпросмотр */} + - {/* Медиа в превью (если есть) */} - {articleMedia && ( - - Превью медиа - - )} - {!articleMedia && ( - - - - )} - - {/* Заголовок в превью */} - Предпросмотр + - + Превью медиа + + ) : ( + + + + )} + + {/* Заголовок в превью */} + - {articleTitle || "Название информации"} - - + + {data.left.heading || "Название информации"} + + - {/* Текст статьи в превью */} - + + + + + - flexGrow: 1, - }} - > - - - + + - - - - - - ); -}; + + + ); + } +); diff --git a/src/widgets/SightTabs/RightWidgetTab/index.tsx b/src/widgets/SightTabs/RightWidgetTab/index.tsx index c8be167..53c80af 100644 --- a/src/widgets/SightTabs/RightWidgetTab/index.tsx +++ b/src/widgets/SightTabs/RightWidgetTab/index.tsx @@ -1,405 +1,404 @@ +// RightWidgetTab.tsx +import { Box, Button, Paper, TextField, Typography } from "@mui/material"; import { - Box, - Button, - List, - ListItemButton, - ListItemText, - Paper, - Typography, - Menu, - MenuItem, - Dialog, - DialogTitle, - DialogContent, - DialogActions, - TextField, - InputAdornment, -} from "@mui/material"; -import { - articlesStore, - BackButton, - SelectArticleModal, - Sight, TabPanel, + BackButton, + languageStore, // Предполагаем, что он есть в @shared + Language, // Предполагаем, что он есть в @shared + // SelectArticleModal, // Добавим позже + // articlesStore, // Добавим позже } from "@shared"; -import { SightEdit } from "@widgets"; -import { ImagePlus, Plus, Search } from "lucide-react"; +import { LanguageSwitcher } from "@widgets"; // Предполагаем, что LanguageSwitcher у вас есть import { observer } from "mobx-react-lite"; -import { useState, useEffect, useRef } from "react"; +import { useState, useMemo, useEffect } from "react"; +import { editSightStore, BlockItem } from "@shared"; // Путь к вашему стору -// --- Mock Data (can be moved to a separate file or fetched from an API) --- -const mockRightWidgetBlocks = [ - { id: "preview_media", name: "Превью-медиа", type: "special" }, - { id: "article_1", name: "1. История", type: "article" }, - { id: "article_2", name: "2. Факты", type: "article" }, - { - id: "article_3", - name: "3. Блокада (Пример длинного названия)", - type: "article", - }, +// Импортируем сюда же определения BlockItem, если не выносим в types.ts +// export interface BlockItem { id: string; type: 'media' | 'article'; nameForSidebar: string; linkedArticleStoreId?: string; } + +// --- Начальные данные для структуры блоков (позже это может загружаться) --- +// ID здесь должны быть уникальными для списка. +const initialBlockStructures: Omit[] = [ + { id: "preview_media_main", type: "media" }, + { id: "article_1_local", type: "article" }, // Эти статьи будут редактироваться локально + { id: "article_2_local", type: "article" }, ]; -const mockSelectedBlockData = { - id: "article_1", - heading: "История основания Санкт-Петербурга", - body: "## Начало\nГород был основан 27 мая 1703 года Петром I...", - media: [], -}; - -const mockExistingArticles = [ - { id: "existing_1", title: "История Эрмитажа", type: "article" }, - { id: "existing_2", title: "Петропавловская крепость", type: "article" }, - { id: "existing_3", title: "Исаакиевский собор", type: "article" }, - { id: "existing_4", title: "Кунсткамера", type: "article" }, -]; - -// --- ArticleListSidebar Component --- -interface ArticleBlock { - id: string; - name: string; - type: string; - linkedArticleId?: string; // Added for linked articles +interface RightWidgetTabProps { + value: number; + index: number; } -interface ArticleListSidebarProps { - blocks: ArticleBlock[]; - selectedBlockId: string | null; - onSelectBlock: (blockId: string) => void; - onCreateNew: () => void; - onSelectExisting: () => void; -} - -const ArticleListSidebar = ({ - blocks, - selectedBlockId, - onSelectBlock, - onCreateNew, - onSelectExisting, -}: ArticleListSidebarProps) => { - const [menuAnchorEl, setMenuAnchorEl] = useState(null); - - const handleMenuOpen = (event: React.MouseEvent) => { - setMenuAnchorEl(event.currentTarget); - }; - - const handleMenuClose = () => { - setMenuAnchorEl(null); - }; - - return ( - - - {blocks.map((block) => ( - onSelectBlock(block.id)} - sx={{ - borderRadius: 1, - mb: 0.5, - backgroundColor: - selectedBlockId === block.id ? "primary.light" : "transparent", - "&.Mui-selected": { - backgroundColor: "primary.main", - color: "primary.contrastText", - "&:hover": { - backgroundColor: "primary.dark", - }, - }, - "&:hover": { - backgroundColor: - selectedBlockId !== block.id ? "action.hover" : undefined, - }, - }} - > - - - ))} - - - - - Создать новую - Выбрать существующую - - - ); -}; - -// --- ArticleEditorPane Component --- -interface ArticleData { - id: string; - heading: string; - body: string; - media: any[]; // Define a proper type for media if available -} - -interface ArticleEditorPaneProps { - articleData: ArticleData | null; - onDelete: (blockId: string) => void; -} - -const ArticleEditorPane = ({ - articleData, - onDelete, -}: ArticleEditorPaneProps) => { - if (!articleData) { - return ( - - - Выберите блок для редактирования - - - ); - } - - return ( - - - - - МЕДИА - - - Нет медиа - - - - - ); -}; - -// --- RightWidgetTab (Parent) Component --- export const RightWidgetTab = observer( - ({ value, index, data }: { value: number; index: number; data?: Sight }) => { - const [rightWidgetBlocks, setRightWidgetBlocks] = useState( - mockRightWidgetBlocks - ); - const [selectedBlockId, setSelectedBlockId] = useState( - mockRightWidgetBlocks[1]?.id || null - ); - const [isSelectModalOpen, setIsSelectModalOpen] = useState(false); + ({ value, index }: RightWidgetTabProps) => { + const { language } = languageStore; // Текущий язык + const { sightInfo } = editSightStore; // Данные достопримечательности + // 1. Структура блоков: порядок, тип, связи (не сам контент) + // Имена nameForSidebar будут динамически браться из sightInfo или articlesStore + const [blockItemsStructure, setBlockItemsStructure] = useState< + Omit[] + >(initialBlockStructures); + + // 2. ID выбранного блока для редактирования + const [selectedBlockId, setSelectedBlockId] = useState( + () => { + // По умолчанию выбираем первый блок, если он есть + return initialBlockStructures.length > 0 + ? initialBlockStructures[0].id + : null; + } + ); + + // 3. Состояние для модального окна выбора существующей статьи (добавим позже) + // const [isSelectModalOpen, setIsSelectModalOpen] = useState(false); + + // --- Производные данные (Derived State) --- + + // Блоки для отображения в сайдбаре (с локализованными именами) + const blocksForSidebar: BlockItem[] = useMemo(() => { + return blockItemsStructure.map((struct) => { + let name = `Блок ${struct.id}`; // Имя по умолчанию + + if (struct.type === "media" && struct.id === "preview_media_main") { + name = "Превью-медиа"; // Фиксированное имя для этого блока + } else if (struct.type === "article") { + if (struct.linkedArticleStoreId) { + // TODO: Найти имя в articlesStore по struct.linkedArticleStoreId + name = `Связанная: ${struct.linkedArticleStoreId}`; + } else { + // Это локальная статья, берем заголовок из editSightStore + const articleContent = sightInfo[language]?.right?.find( + (a) => a.id === struct.id + ); + name = + articleContent?.heading || + `Статья ${struct.id.slice(-4)} (${language.toUpperCase()})`; + } + } + return { ...struct, nameForSidebar: name }; + }); + }, [blockItemsStructure, language, sightInfo]); + + // Данные выбранного блока (структура + контент) + const selectedBlockData = useMemo(() => { + if (!selectedBlockId) return null; + const structure = blockItemsStructure.find( + (b) => b.id === selectedBlockId + ); + if (!structure) return null; + + if (structure.type === "article" && !structure.linkedArticleStoreId) { + const content = sightInfo[language]?.right?.find( + (a) => a.id === selectedBlockId + ); + return { + structure, + content: content || { id: selectedBlockId, heading: "", body: "" }, // Заглушка, если нет контента + }; + } + // Для media или связанных статей пока просто структура + return { structure, content: null }; + }, [selectedBlockId, blockItemsStructure, language, sightInfo]); + + // --- Обработчики событий --- const handleSelectBlock = (blockId: string) => { setSelectedBlockId(blockId); - console.log("Selected block:", blockId); }; - const handleCreateNew = () => { - const newBlockId = `article_${Date.now()}`; - setRightWidgetBlocks((prevBlocks) => [ - ...prevBlocks, - { - id: newBlockId, - name: `${ - prevBlocks.filter((b) => b.type === "article").length + 1 - }. Новый блок`, - type: "article", - }, - ]); + const handleCreateNewArticle = () => { + const newBlockId = `article_local_${Date.now()}`; + const newBlockStructure: Omit = { + id: newBlockId, + type: "article", + }; + setBlockItemsStructure((prev) => [...prev, newBlockStructure]); + + // Добавляем пустой контент для этой статьи во все языки в editSightStore + const baseName = `Новая статья ${ + blockItemsStructure.filter((b) => b.type === "article").length + 1 + }`; + ["ru", "en", "zh"].forEach((lang) => { + const currentLang = lang as Language; + if ( + editSightStore.sightInfo[currentLang] && + !editSightStore.sightInfo[currentLang].right?.find( + (r) => r.id === newBlockId + ) + ) { + editSightStore.sightInfo[currentLang].right.push({ + id: newBlockId, + heading: `${baseName} (${currentLang.toUpperCase()})`, + body: `Содержимое для ${baseName} (${currentLang.toUpperCase()})...`, + }); + } + }); setSelectedBlockId(newBlockId); }; - const handleSelectExisting = () => { - setIsSelectModalOpen(true); - }; - - const handleCloseSelectModal = () => { - setIsSelectModalOpen(false); - }; - - const handleSelectArticle = (articleId: string) => { - const article = articlesStore.articles.find((a) => a.id === articleId); - if (article) { - const newBlockId = `article_linked_${article.id}_${Date.now()}`; - setRightWidgetBlocks((prevBlocks) => [ - ...prevBlocks, - { - id: newBlockId, - name: `${ - prevBlocks.filter((b) => b.type === "article").length + 1 - }. ${article.service_name}`, - type: "article", - linkedArticleId: article.id, - }, - ]); - setSelectedBlockId(newBlockId); + const handleHeadingChange = (newHeading: string) => { + if ( + selectedBlockData && + selectedBlockData.structure.type === "article" && + !selectedBlockData.structure.linkedArticleStoreId + ) { + const blockId = selectedBlockData.structure.id; + const langData = editSightStore.sightInfo[language]; + const article = langData?.right?.find((a) => a.id === blockId); + if (article) { + article.heading = newHeading; + } else if (langData) { + // Если статьи еще нет, добавляем + langData.right.push({ id: blockId, heading: newHeading, body: "" }); + } + // Обновить имя в сайдбаре (т.к. blocksForSidebar пересчитается) + // Для этого достаточно, чтобы sightInfo был observable и blocksForSidebar от него зависел } - handleCloseSelectModal(); }; - const handleUnlinkBlock = (blockId: string) => { - console.log("Unlink block:", blockId); - // Example: If a block is linked to an existing article, this might "unlink" it - // For now, it simply removes it, you might want to convert it to a new editable block. - setRightWidgetBlocks((blocks) => blocks.filter((b) => b.id !== blockId)); - setSelectedBlockId(null); + const handleBodyChange = (newBody: string) => { + if ( + selectedBlockData && + selectedBlockData.structure.type === "article" && + !selectedBlockData.structure.linkedArticleStoreId + ) { + const blockId = selectedBlockData.structure.id; + const langData = editSightStore.sightInfo[language]; + const article = langData?.right?.find((a) => a.id === blockId); + if (article) { + article.body = newBody; + } else if (langData) { + // Если статьи еще нет, добавляем + langData.right.push({ id: blockId, heading: "", body: newBody }); + } + } }; - const handleDeleteBlock = (blockId: string) => { - console.log("Delete block:", blockId); - setRightWidgetBlocks((blocks) => blocks.filter((b) => b.id !== blockId)); - setSelectedBlockId(null); + const handleDeleteBlock = (blockIdToDelete: string) => { + setBlockItemsStructure((prev) => + prev.filter((b) => b.id !== blockIdToDelete) + ); + // Удаляем контент из editSightStore для всех языков + ["ru", "en", "zh"].forEach((lang) => { + const currentLang = lang as Language; + if (editSightStore.sightInfo[currentLang]) { + editSightStore.sightInfo[currentLang].right = + editSightStore.sightInfo[currentLang].right?.filter( + (r) => r.id !== blockIdToDelete + ); + } + }); + + if (selectedBlockId === blockIdToDelete) { + setSelectedBlockId( + blockItemsStructure.length > 1 + ? blockItemsStructure.filter((b) => b.id !== blockIdToDelete)[0]?.id + : null + ); + } }; const handleSave = () => { - console.log("Saving right widget..."); - // Implement save logic here, e.g., send data to an API + console.log( + "Сохранение Right Widget:", + JSON.stringify(editSightStore.sightInfo, null, 2) + ); + // Здесь будет логика отправки editSightStore.sightInfo на сервер + alert("Данные для сохранения (см. консоль)"); }; - // Determine the current block data to pass to the editor pane - const currentBlockToEdit = selectedBlockId - ? selectedBlockId === mockSelectedBlockData.id - ? mockSelectedBlockData - : { - id: selectedBlockId, - heading: - rightWidgetBlocks.find((b) => b.id === selectedBlockId)?.name || - "Заголовок...", - body: "Содержимое...", - media: [], - } - : null; - - // Get list of already linked article IDs - const linkedArticleIds = rightWidgetBlocks - .filter((block) => block.linkedArticleId) - .map((block) => block.linkedArticleId as string); + // --- Инициализация контента в сторе для initialBlockStructures (если его там нет) --- + useEffect(() => { + initialBlockStructures.forEach((struct) => { + if (struct.type === "article" && !struct.linkedArticleStoreId) { + const baseName = `Статья ${struct.id.split("_")[1]}`; // Пример "История" или "Факты" + ["ru", "en", "zh"].forEach((lang) => { + const currentLang = lang as Language; + if ( + editSightStore.sightInfo[currentLang] && + !editSightStore.sightInfo[currentLang].right?.find( + (r) => r.id === struct.id + ) + ) { + editSightStore.sightInfo[currentLang].right?.push({ + id: struct.id, + heading: `${baseName} (${currentLang.toUpperCase()})`, // Например: "История (RU)" + body: `Начальное содержимое для ${baseName} на ${currentLang.toUpperCase()}.`, + }); + } + }); + } + }); + }, []); // Запускается один раз при монтировании return ( + - - + + {/* Компонент сайдбара списка блоков */} + + + Блоки + + + {blocksForSidebar.map((block) => ( + + ))} + + + {/* TODO: Кнопка "Выбрать существующую" */} + - + {/* Компонент редактора выбранного блока */} + + + Редактор блока ({language.toUpperCase()}) + + {selectedBlockData ? ( + + + ID: {selectedBlockData.structure.id} + + + Тип: {selectedBlockData.structure.type} + + {selectedBlockData.structure.type === "media" && ( + + + Загрузчик медиа для "{selectedBlockData.structure.id}" + + + )} + {selectedBlockData.structure.type === "article" && + !selectedBlockData.structure.linkedArticleStoreId && + selectedBlockData.content && ( + + handleHeadingChange(e.target.value)} + sx={{ mb: 2 }} + /> + handleBodyChange(e.target.value)} + sx={{ mb: 2 }} + // Здесь позже можно будет вставить SightEdit + /> + {/* TODO: Секция медиа для статьи */} + + + )} + {selectedBlockData.structure.type === "article" && + selectedBlockData.structure.linkedArticleStoreId && ( + + + Это связанная статья:{" "} + {selectedBlockData.structure.linkedArticleStoreId} + + {/* TODO: Кнопки "Открепить", "Удалить из списка" */} + + )} + + ) : ( + + Выберите блок для редактирования + + )} + - - - + {/* */} ); } diff --git a/src/widgets/index.ts b/src/widgets/index.ts index c21b92f..5db03b3 100644 --- a/src/widgets/index.ts +++ b/src/widgets/index.ts @@ -7,3 +7,4 @@ export * from "./SightEdit"; export * from "./LanguageSwitcher"; export * from "./DevicesTable"; export * from "./SightsTable"; +export * from "./MediaViewer"; diff --git a/src/widgets/modals/SelectArticleDialog/index.tsx b/src/widgets/modals/SelectArticleDialog/index.tsx new file mode 100644 index 0000000..e59b718 --- /dev/null +++ b/src/widgets/modals/SelectArticleDialog/index.tsx @@ -0,0 +1,188 @@ +import { articlesStore } from "@shared"; +import { observer } from "mobx-react-lite"; +import { useEffect, useRef, useState } from "react"; +import { + Dialog, + DialogTitle, + DialogContent, + DialogActions, + Button, + TextField, + List, + ListItemButton, + ListItemText, + Paper, + Box, + Typography, + InputAdornment, +} from "@mui/material"; +import { ImagePlus, Search } from "lucide-react"; +import { ReactMarkdownComponent } from "@widgets"; + +interface SelectArticleModalProps { + open: boolean; + onClose: () => void; + onSelectArticle: (articleId: string) => void; + linkedArticleIds?: string[]; // Add optional prop for linked articles +} + +export const SelectArticleModal = observer( + ({ + open, + onClose, + onSelectArticle, + + linkedArticleIds = [], // Default to empty array if not provided + }: SelectArticleModalProps) => { + const { articles, getArticle } = articlesStore; // articles here refers to fetched articles from store + const [searchQuery, setSearchQuery] = useState(""); + const [hoveredArticleId, setHoveredArticleId] = useState( + null + ); + const hoverTimerRef = useRef(null); + + useEffect(() => { + if (hoveredArticleId) { + hoverTimerRef.current = setTimeout(() => { + getArticle(hoveredArticleId); + }, 200); + } + + return () => { + if (hoverTimerRef.current) { + clearTimeout(hoverTimerRef.current); + } + }; + }, [hoveredArticleId, getArticle]); + + const handleArticleHover = (articleId: string) => { + setHoveredArticleId(articleId); + }; + + const handleArticleLeave = () => { + setHoveredArticleId(null); + if (hoverTimerRef.current) { + clearTimeout(hoverTimerRef.current); + } + }; + + const filteredArticles = articles + .filter((article) => !linkedArticleIds.includes(article.id)) + .filter((article) => + article.service_name.toLowerCase().includes(searchQuery.toLowerCase()) + ); + + return ( + + Выберите существующую статью + + + setSearchQuery(e.target.value)} + sx={{ mb: 2, mt: 1 }} + InputProps={{ + startAdornment: ( + + + + ), + }} + /> + + {filteredArticles.map((article) => ( + onSelectArticle(article.id)} + onMouseEnter={() => handleArticleHover(article.id)} + onMouseLeave={handleArticleLeave} + sx={{ + borderRadius: 1, + mb: 0.5, + "&:hover": { + backgroundColor: "action.hover", + }, + }} + > + + + ))} + + + + + {/* Media Preview Area */} + + + + + {/* Title Area */} + + + {articlesStore.articleData?.heading || + "Нет данных для предпросмотра"} + + + + {/* Body Preview Area */} + + + + + + + + + + + ); + } +); diff --git a/src/widgets/modals/index.ts b/src/widgets/modals/index.ts new file mode 100644 index 0000000..e714367 --- /dev/null +++ b/src/widgets/modals/index.ts @@ -0,0 +1 @@ +export * from "./SelectArticleDialog"; diff --git a/yarn.lock b/yarn.lock index f7ee7ec..84388b2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -138,6 +138,11 @@ resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.3.tgz" integrity sha512-7EYtGezsdiDMyY80+65EzwiGmcJqpmcZCojSXaRgdrBaGtWTgDZKq69cPIVped6MkIM78cTQ2GOiEYjwOlG4xw== +"@babel/runtime@^7.17.8", "@babel/runtime@^7.26.0": + version "7.27.4" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.27.4.tgz#a91ec580e6c00c67118127777c316dfd5a5a6abf" + integrity sha512-t3yaEOuGu9NlIZ+hIeGbBjFtZT7j2cb2tg0fuaJKeGotchRjjLfrBA9Kwf8quhpP1EUuxModQg04q/mBwyg8uA== + "@babel/template@^7.27.2": version "7.27.2" resolved "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz" @@ -168,6 +173,11 @@ "@babel/helper-string-parser" "^7.27.1" "@babel/helper-validator-identifier" "^7.27.1" +"@dimforge/rapier3d-compat@^0.12.0": + version "0.12.0" + resolved "https://registry.yarnpkg.com/@dimforge/rapier3d-compat/-/rapier3d-compat-0.12.0.tgz#7b3365e1dfdc5cd957b45afe920b4ac06c7cd389" + integrity sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow== + "@emnapi/core@^1.4.3": version "1.4.3" resolved "https://registry.yarnpkg.com/@emnapi/core/-/core-1.4.3.tgz#9ac52d2d5aea958f67e52c40a065f51de59b77d6" @@ -555,6 +565,18 @@ "@jridgewell/resolve-uri" "^3.1.0" "@jridgewell/sourcemap-codec" "^1.4.14" +"@mediapipe/tasks-vision@0.10.17": + version "0.10.17" + resolved "https://registry.yarnpkg.com/@mediapipe/tasks-vision/-/tasks-vision-0.10.17.tgz#2c1c73ed81902b21d37336a587b96183bb6882d5" + integrity sha512-CZWV/q6TTe8ta61cZXjfnnHsfWIdFhms03M9T7Cnd5y2mdpylJM0rF1qRq+wsQVRMLz1OYPVEBU9ph2Bx8cxrg== + +"@monogrid/gainmap-js@^3.0.6": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@monogrid/gainmap-js/-/gainmap-js-3.1.0.tgz#4ac1f88abd6affdf0b51d79318109183b499c502" + integrity sha512-Obb0/gEd/HReTlg8ttaYk+0m62gQJmCblMOjHSMHRrBP2zdfKMHLCRbh/6ex9fSUJMKdjjIEiohwkbGD3wj2Nw== + dependencies: + promise-worker-transferable "^1.0.4" + "@mui/core-downloads-tracker@^7.1.0": version "7.1.0" resolved "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-7.1.0.tgz" @@ -662,11 +684,63 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" +"@photo-sphere-viewer/core@^5.13.2": + version "5.13.2" + resolved "https://registry.yarnpkg.com/@photo-sphere-viewer/core/-/core-5.13.2.tgz#518f27a2b7ca5a80068d8922183a9999a1b33ad1" + integrity sha512-rL4Ey39Prx4Iyxt1f2tAqlXvqu4/ovXfUvIpLt540OpZJiFjWccs6qLywof9vuhBJ7PXHudHWCjRPce0W8kx8w== + dependencies: + three "^0.175.0" + "@popperjs/core@^2.11.8": version "2.11.8" resolved "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz" integrity sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A== +"@react-three/drei@^10.1.2": + version "10.1.2" + resolved "https://registry.yarnpkg.com/@react-three/drei/-/drei-10.1.2.tgz#3c41a0b19460aee7604067309cebe737147cf85a" + integrity sha512-CCcLAqZEvYiUErOcJgGzovY3RH6KgdrqD4ubeAx1nyGbSPLnKR9a8ynYbPdtZhIyiWqGc07z+RYQzpaOfN4ZIA== + dependencies: + "@babel/runtime" "^7.26.0" + "@mediapipe/tasks-vision" "0.10.17" + "@monogrid/gainmap-js" "^3.0.6" + "@use-gesture/react" "^10.3.1" + camera-controls "^2.9.0" + cross-env "^7.0.3" + detect-gpu "^5.0.56" + glsl-noise "^0.0.0" + hls.js "^1.5.17" + maath "^0.10.8" + meshline "^3.3.1" + stats-gl "^2.2.8" + stats.js "^0.17.0" + suspend-react "^0.1.3" + three-mesh-bvh "^0.8.3" + three-stdlib "^2.35.6" + troika-three-text "^0.52.4" + tunnel-rat "^0.1.2" + use-sync-external-store "^1.4.0" + utility-types "^3.11.0" + zustand "^5.0.1" + +"@react-three/fiber@^9.1.2": + version "9.1.2" + resolved "https://registry.yarnpkg.com/@react-three/fiber/-/fiber-9.1.2.tgz#c988f3aa916f64771483784ca3bb6ba4b116395e" + integrity sha512-k8FR9yVHV9kIF3iuOD0ds5hVymXYXfgdKklqziBVod9ZEJ8uk05Zjw29J/omU3IKeUfLNAIHfxneN3TUYM4I2w== + dependencies: + "@babel/runtime" "^7.17.8" + "@types/react-reconciler" "^0.28.9" + "@types/webxr" "*" + base64-js "^1.5.1" + buffer "^6.0.3" + its-fine "^2.0.0" + react-reconciler "^0.31.0" + react-use-measure "^2.1.7" + scheduler "^0.25.0" + suspend-react "^0.1.3" + use-sync-external-store "^1.4.0" + zustand "^5.0.3" + "@rolldown/pluginutils@1.0.0-beta.9": version "1.0.0-beta.9" resolved "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.9.tgz" @@ -892,6 +966,11 @@ "@tailwindcss/oxide" "4.1.8" tailwindcss "4.1.8" +"@tweenjs/tween.js@~23.1.3": + version "23.1.3" + resolved "https://registry.yarnpkg.com/@tweenjs/tween.js/-/tween.js-23.1.3.tgz#eff0245735c04a928bb19c026b58c2a56460539d" + integrity sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA== + "@tybys/wasm-util@^0.9.0": version "0.9.0" resolved "https://registry.yarnpkg.com/@tybys/wasm-util/-/wasm-util-0.9.0.tgz#3e75eb00604c8d6db470bf18c37b7d984a0e3355" @@ -946,6 +1025,11 @@ dependencies: "@types/ms" "*" +"@types/draco3d@^1.4.0": + version "1.4.10" + resolved "https://registry.yarnpkg.com/@types/draco3d/-/draco3d-1.4.10.tgz#63ec0ba78b30bd58203ec031f4e4f0198c596dca" + integrity sha512-AX22jp8Y7wwaBgAixaSvkoG4M/+PlAcm3Qs4OW8yT9DM4xUpWKeFhLueTAyZF39pviAdcDdeJoACapiAceqNcw== + "@types/estree-jsx@^1.0.0": version "1.0.5" resolved "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz" @@ -994,6 +1078,11 @@ dependencies: undici-types "~6.21.0" +"@types/offscreencanvas@^2019.6.4": + version "2019.7.3" + resolved "https://registry.yarnpkg.com/@types/offscreencanvas/-/offscreencanvas-2019.7.3.tgz#90267db13f64d6e9ccb5ae3eac92786a7c77a516" + integrity sha512-ieXiYmgSRXUDeOntE1InxjWyvEelZGP63M+cGuquuRLuIKKT1osnkXjxev9B7d1nXSug5vpunx+gNlbVxMlC9A== + "@types/parse-json@^4.0.0": version "4.0.2" resolved "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz" @@ -1009,6 +1098,11 @@ resolved "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.5.tgz" integrity sha512-CMCjrWucUBZvohgZxkjd6S9h0nZxXjzus6yDfUb+xLxYM7VvjKNH1tQrE9GWLql1XoOP4/Ds3bwFqShHUYraGg== +"@types/react-reconciler@^0.28.9": + version "0.28.9" + resolved "https://registry.yarnpkg.com/@types/react-reconciler/-/react-reconciler-0.28.9.tgz#d24b4864c384e770c83275b3fe73fba00269c83b" + integrity sha512-HHM3nxyUZ3zAylX8ZEyrDNd2XZOnQ0D5XfunJF5FLQnZbHHYq4UWvW1QfelQNXv1ICNkwYhfxjwfnqivYB6bFg== + "@types/react-transition-group@^4.4.12": version "4.4.12" resolved "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz" @@ -1021,6 +1115,11 @@ dependencies: csstype "^3.0.2" +"@types/stats.js@*": + version "0.17.4" + resolved "https://registry.yarnpkg.com/@types/stats.js/-/stats.js-0.17.4.tgz#1933e5ff153a23c7664487833198d685c22e791e" + integrity sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA== + "@types/tern@*": version "0.23.9" resolved "https://registry.npmjs.org/@types/tern/-/tern-0.23.9.tgz" @@ -1028,6 +1127,19 @@ dependencies: "@types/estree" "*" +"@types/three@*": + version "0.176.0" + resolved "https://registry.yarnpkg.com/@types/three/-/three-0.176.0.tgz#b6eced2b05e839395a6171e066c4631bc5b0a1e0" + integrity sha512-FwfPXxCqOtP7EdYMagCFePNKoG1AGBDUEVKtluv2BTVRpSt7b+X27xNsirPCTCqY1pGYsPUzaM3jgWP7dXSxlw== + dependencies: + "@dimforge/rapier3d-compat" "^0.12.0" + "@tweenjs/tween.js" "~23.1.3" + "@types/stats.js" "*" + "@types/webxr" "*" + "@webgpu/types" "*" + fflate "~0.8.2" + meshoptimizer "~0.18.1" + "@types/unist@*", "@types/unist@^3.0.0": version "3.0.3" resolved "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz" @@ -1038,6 +1150,11 @@ resolved "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz" integrity sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA== +"@types/webxr@*", "@types/webxr@^0.5.2": + version "0.5.22" + resolved "https://registry.yarnpkg.com/@types/webxr/-/webxr-0.5.22.tgz#d8a14c12bbfaaa4a13de21ec2d4a8197b3e1b532" + integrity sha512-Vr6Stjv5jPRqH690f5I5GLjVk8GSsoQSYJ2FVd/3jJF7KaqfwPi3ehfBS96mlQ2kPCwZaX6U0rG2+NGHBKkA/A== + "@typescript-eslint/eslint-plugin@8.33.0": version "8.33.0" resolved "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.33.0.tgz" @@ -1140,6 +1257,18 @@ resolved "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz" integrity sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g== +"@use-gesture/core@10.3.1": + version "10.3.1" + resolved "https://registry.yarnpkg.com/@use-gesture/core/-/core-10.3.1.tgz#976c9421e905f0079d49822cfd5c2e56b808fc56" + integrity sha512-WcINiDt8WjqBdUXye25anHiNxPc0VOrlT8F6LLkU6cycrOGUDyY/yyFmsg3k8i5OLvv25llc0QC45GhR/C8llw== + +"@use-gesture/react@^10.3.1": + version "10.3.1" + resolved "https://registry.yarnpkg.com/@use-gesture/react/-/react-10.3.1.tgz#17a743a894d9bd9a0d1980c618f37f0164469867" + integrity sha512-Yy19y6O2GJq8f7CHf7L0nxL8bf4PZCPaVOCgJrusOeFHY1LvHgYXnmnXg6N5iwAnbgbZCDjo60SiM6IPJi9C5g== + dependencies: + "@use-gesture/core" "10.3.1" + "@vitejs/plugin-react@^4.4.1": version "4.5.0" resolved "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.5.0.tgz" @@ -1152,6 +1281,11 @@ "@types/babel__core" "^7.20.5" react-refresh "^0.17.0" +"@webgpu/types@*": + version "0.1.61" + resolved "https://registry.yarnpkg.com/@webgpu/types/-/types-0.1.61.tgz#60ac1756bbeeae778b5357a94d4e6e160592d6f1" + integrity sha512-w2HbBvH+qO19SB5pJOJFKs533CdZqxl3fcGonqL321VHkW7W/iBo6H8bjDy6pr/+pbMwIu5dnuaAxH7NxBqUrQ== + acorn-jsx@^5.3.2: version "5.3.2" resolved "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz" @@ -1217,6 +1351,18 @@ balanced-match@^1.0.0: resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz" integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== +base64-js@^1.3.1, base64-js@^1.5.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" + integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== + +bidi-js@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/bidi-js/-/bidi-js-1.0.3.tgz#6f8bcf3c877c4d9220ddf49b9bb6930c88f877d2" + integrity sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw== + dependencies: + require-from-string "^2.0.2" + brace-expansion@^1.1.7: version "1.1.11" resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz" @@ -1249,6 +1395,14 @@ browserslist@^4.24.0: node-releases "^2.0.19" update-browserslist-db "^1.1.3" +buffer@^6.0.3: + version "6.0.3" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6" + integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA== + dependencies: + base64-js "^1.3.1" + ieee754 "^1.2.1" + call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz#4b5428c222be985d79c3d82657479dbe0b59b2d6" @@ -1262,6 +1416,11 @@ callsites@^3.0.0: resolved "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz" integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== +camera-controls@^2.9.0: + version "2.10.1" + resolved "https://registry.yarnpkg.com/camera-controls/-/camera-controls-2.10.1.tgz#78bc58001a2d5925c29312154ce77d16967dec56" + integrity sha512-KnaKdcvkBJ1Irbrzl8XD6WtZltkRjp869Jx8c0ujs9K+9WD+1D7ryBsCiVqJYUqt6i/HR5FxT7RLASieUD+Q5w== + caniuse-lite@^1.0.30001716: version "1.0.30001718" resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001718.tgz" @@ -1377,7 +1536,14 @@ cosmiconfig@^7.0.0: path-type "^4.0.0" yaml "^1.10.0" -cross-spawn@^7.0.6: +cross-env@^7.0.3: + version "7.0.3" + resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-7.0.3.tgz#865264b29677dc015ba8418918965dd232fc54cf" + integrity sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw== + dependencies: + cross-spawn "^7.0.1" + +cross-spawn@^7.0.1, cross-spawn@^7.0.6: version "7.0.6" resolved "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz" integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA== @@ -1425,6 +1591,13 @@ dequal@^2.0.0: resolved "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz" integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA== +detect-gpu@^5.0.56: + version "5.0.70" + resolved "https://registry.yarnpkg.com/detect-gpu/-/detect-gpu-5.0.70.tgz#db2202d3cd440714ba6e789ff8b62d1b584eabf7" + integrity sha512-bqerEP1Ese6nt3rFkwPnGbsUF9a4q+gMmpTVVOEzoCyeCc+y7/RvJnQZJx1JwhgQI5Ntg0Kgat8Uu7XpBqnz1w== + dependencies: + webgl-constants "^1.1.1" + detect-libc@^2.0.3, detect-libc@^2.0.4: version "2.0.4" resolved "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz" @@ -1445,6 +1618,11 @@ dom-helpers@^5.0.1: "@babel/runtime" "^7.8.7" csstype "^3.0.2" +draco3d@^1.4.1: + version "1.5.7" + resolved "https://registry.yarnpkg.com/draco3d/-/draco3d-1.5.7.tgz#94f9bce293eb8920c159dc91a4ce9124a9e899e0" + integrity sha512-m6WCKt/erDXcw+70IJXnG7M3awwQPAsZvJGX5zY7beBqpELw6RDGkYVU0W43AFxye4pDZ5i2Lbyc/NNGqwjUVQ== + dunder-proto@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/dunder-proto/-/dunder-proto-1.0.1.tgz#d7ae667e1dc83482f8b70fd0f6eefc50da30f58a" @@ -1665,6 +1843,11 @@ esutils@^2.0.2: resolved "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz" integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== +eventemitter3@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-5.0.1.tgz#53f5ffd0a492ac800721bb42c66b841de96423c4" + integrity sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA== + extend@^3.0.0: version "3.0.2" resolved "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz" @@ -1708,6 +1891,16 @@ fdir@^6.4.4: resolved "https://registry.npmjs.org/fdir/-/fdir-6.4.5.tgz" integrity sha512-4BG7puHpVsIYxZUbiUE3RqGloLaSSwzYie5jvasC4LWuBWzZawynvYouhjbQKw2JuIGYdm0DzIxl8iVidKlUEw== +fflate@^0.6.9: + version "0.6.10" + resolved "https://registry.yarnpkg.com/fflate/-/fflate-0.6.10.tgz#5f40f9659205936a2d18abf88b2e7781662b6d43" + integrity sha512-IQrh3lEPM93wVCEczc9SaAOvkmcoQn/G8Bo1e8ZPlY3X3bnAxWaBdvTdvM1hP62iZp0BXWDy4vTAy4fF0+Dlpg== + +fflate@~0.8.2: + version "0.8.2" + resolved "https://registry.yarnpkg.com/fflate/-/fflate-0.8.2.tgz#fc8631f5347812ad6028bbe4a2308b2792aa1dea" + integrity sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A== + file-entry-cache@^8.0.0: version "8.0.0" resolved "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz" @@ -1831,6 +2024,11 @@ globals@^16.0.0: resolved "https://registry.npmjs.org/globals/-/globals-16.2.0.tgz" integrity sha512-O+7l9tPdHCU320IigZZPj5zmRCFG9xHmx9cU8FqU2Rp+JN714seHV+2S9+JslCpY4gJwU2vOGox0wzgae/MCEg== +glsl-noise@^0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/glsl-noise/-/glsl-noise-0.0.0.tgz#367745f3a33382c0eeec4cb54b7e99cfc1d7670b" + integrity sha512-b/ZCF6amfAUb7dJM/MxRs7AetQEahYzJ8PtgfrmEdtw6uyGOr+ZSGtgjFm6mfsBkxJ4d2W7kg+Nlqzqvn3Bc0w== + gopd@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1" @@ -1962,6 +2160,11 @@ hastscript@^9.0.0: property-information "^7.0.0" space-separated-tokens "^2.0.0" +hls.js@^1.5.17: + version "1.6.4" + resolved "https://registry.yarnpkg.com/hls.js/-/hls.js-1.6.4.tgz#0c4070f5f719eda2687e2ab13b061dbb01967dd8" + integrity sha512-sxFS61suCMJBvpOhmi4WLnarOZ8S8JAxK5J1icvrkopE8aRMc1pRB9WZWMX5Obh9nieVEML6uLLeyGksapyX5A== + hoist-non-react-statics@^3.3.1: version "3.3.2" resolved "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz" @@ -1979,6 +2182,11 @@ html-void-elements@^3.0.0: resolved "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz" integrity sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg== +ieee754@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" + integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== + ignore@^5.2.0: version "5.3.2" resolved "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz" @@ -1989,6 +2197,11 @@ ignore@^7.0.0: resolved "https://registry.npmjs.org/ignore/-/ignore-7.0.4.tgz" integrity sha512-gJzzk+PQNznz8ysRrC0aOkBNVRBDtE1n53IqyqEf3PXrYwomFs5q4pGMizBMJF+ykh03insJ27hB8gSrD2Hn8A== +immediate@~3.0.5: + version "3.0.6" + resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b" + integrity sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ== + import-fresh@^3.2.1: version "3.3.1" resolved "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz" @@ -2069,11 +2282,23 @@ is-plain-obj@^4.0.0: resolved "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz" integrity sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg== +is-promise@^2.1.0: + version "2.2.2" + resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.2.2.tgz#39ab959ccbf9a774cf079f7b40c7a26f763135f1" + integrity sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ== + isexe@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz" integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== +its-fine@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/its-fine/-/its-fine-2.0.0.tgz#a90b18a3ee4c211a1fb6faac2abcc2b682ce1f21" + integrity sha512-KLViCmWx94zOvpLwSlsx6yOCeMhZYaxrJV87Po5k/FoZzcPSahvK5qJ7fYhS61sZi5ikmh2S3Hz55A2l3U69ng== + dependencies: + "@types/react-reconciler" "^0.28.9" + jiti@^2.4.2: version "2.4.2" resolved "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz" @@ -2136,6 +2361,13 @@ levn@^0.4.1: prelude-ls "^1.2.1" type-check "~0.4.0" +lie@^3.0.2: + version "3.3.0" + resolved "https://registry.yarnpkg.com/lie/-/lie-3.3.0.tgz#dcf82dee545f46074daf200c7c1c5a08e0f40f6a" + integrity sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ== + dependencies: + immediate "~3.0.5" + lightningcss-darwin-arm64@1.30.1: version "1.30.1" resolved "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.1.tgz" @@ -2255,6 +2487,11 @@ lucide-react@^0.511.0: resolved "https://registry.npmjs.org/lucide-react/-/lucide-react-0.511.0.tgz" integrity sha512-VK5a2ydJ7xm8GvBeKLS9mu1pVK6ucef9780JVUjw6bAjJL/QXnd4Y0p7SPeOUMC27YhzNCZvm5d/QX0Tp3rc0w== +maath@^0.10.8: + version "0.10.8" + resolved "https://registry.yarnpkg.com/maath/-/maath-0.10.8.tgz#cf647544430141bf6982da6e878abb6c4b804e24" + integrity sha512-tRvbDF0Pgqz+9XUa4jjfgAQ8/aPKmQdWXilFu2tMy4GWj4NOsx99HlULO4IeREfbO3a0sA145DZYyvXPkybm0g== + magic-string@^0.30.17: version "0.30.17" resolved "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz" @@ -2382,6 +2619,16 @@ merge2@^1.3.0: resolved "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz" integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== +meshline@^3.3.1: + version "3.3.1" + resolved "https://registry.yarnpkg.com/meshline/-/meshline-3.3.1.tgz#20decfd5cdd25c8469e862ddf0ab1ad167759734" + integrity sha512-/TQj+JdZkeSUOl5Mk2J7eLcYTLiQm2IDzmlSvYm7ov15anEcDJ92GHqqazxTSreeNgfnYu24kiEvvv0WlbCdFQ== + +meshoptimizer@~0.18.1: + version "0.18.1" + resolved "https://registry.yarnpkg.com/meshoptimizer/-/meshoptimizer-0.18.1.tgz#cdb90907f30a7b5b1190facd3b7ee6b7087797d8" + integrity sha512-ZhoIoL7TNV4s5B6+rx5mC//fw8/POGyNxS/DZyCJeiZ12ScLfVwRE/GfsxwiTkMYYD5DmK2/JXnEVXqL4rF+Sw== + micromark-core-commonmark@^2.0.0: version "2.0.3" resolved "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz" @@ -2787,6 +3034,11 @@ postcss@^8.5.3: picocolors "^1.1.1" source-map-js "^1.2.1" +potpack@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/potpack/-/potpack-1.0.2.tgz#23b99e64eb74f5741ffe7656b5b5c4ddce8dfc14" + integrity sha512-choctRBIV9EMT9WGAZHn3V7t0Z2pMQyl0EZE6pFc/6ml3ssw7Dlf/oAOvFwjm1HVsqfQN8GfeFyJ+d8tRzqueQ== + prelude-ls@^1.2.1: version "1.2.1" resolved "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz" @@ -2797,6 +3049,14 @@ process@^0.11.1: resolved "https://registry.npmjs.org/process/-/process-0.11.10.tgz" integrity sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A== +promise-worker-transferable@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/promise-worker-transferable/-/promise-worker-transferable-1.0.4.tgz#2c72861ba053e5ae42b487b4a83b1ed3ae3786e8" + integrity sha512-bN+0ehEnrXfxV2ZQvU2PetO0n4gqBD4ulq3MI1WOPLgr7/Mg9yRQkX5+0v1vagr74ZTsl7XtzlaYDo2EuCeYJw== + dependencies: + is-promise "^2.1.0" + lie "^3.0.2" + prop-types@^15.6.2, prop-types@^15.8.1: version "15.8.1" resolved "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz" @@ -2865,6 +3125,20 @@ react-markdown@^10.1.0: unist-util-visit "^5.0.0" vfile "^6.0.0" +react-photo-sphere-viewer@^6.2.3: + version "6.2.3" + resolved "https://registry.yarnpkg.com/react-photo-sphere-viewer/-/react-photo-sphere-viewer-6.2.3.tgz#bdbe33a03315077b1d49f2d4690f0dc72563ac9a" + integrity sha512-VzG0aY9CI8OIQjdIoJCjYF1QlnLFpN2pM+zKm1JrpAKQrBZ6B+Uxy94vpVQkGDERgn8FWE0+LIntTgAr60pLyQ== + dependencies: + eventemitter3 "^5.0.1" + +react-reconciler@^0.31.0: + version "0.31.0" + resolved "https://registry.yarnpkg.com/react-reconciler/-/react-reconciler-0.31.0.tgz#6b7390fe8fab59210daf523d7400943973de1458" + integrity sha512-7Ob7Z+URmesIsIVRjnLoDGwBEG/tVitidU0nMsqX/eeJaLY89RISO/10ERe0MqmzuKUUB1rmY+h1itMbUHg9BQ== + dependencies: + scheduler "^0.25.0" + react-refresh@^0.17.0: version "0.17.0" resolved "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz" @@ -2909,6 +3183,11 @@ react-transition-group@^4.4.5: loose-envify "^1.4.0" prop-types "^15.6.2" +react-use-measure@^2.1.7: + version "2.1.7" + resolved "https://registry.yarnpkg.com/react-use-measure/-/react-use-measure-2.1.7.tgz#36b8a2e7fd2fa58109ab851b3addcb0aad66ad1d" + integrity sha512-KrvcAo13I/60HpwGO5jpW7E9DfusKyLPLvuHlUyP5zqnmAPhNc6qTRjUQrdTADl0lpPpDVU2/Gg51UlOGHXbdg== + react@^19.1.0: version "19.1.0" resolved "https://registry.npmjs.org/react/-/react-19.1.0.tgz" @@ -2944,6 +3223,11 @@ remark-rehype@^11.0.0: unified "^11.0.0" vfile "^6.0.0" +require-from-string@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909" + integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== + resolve-from@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz" @@ -2999,6 +3283,11 @@ run-parallel@^1.1.9: dependencies: queue-microtask "^1.2.2" +scheduler@^0.25.0: + version "0.25.0" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.25.0.tgz#336cd9768e8cceebf52d3c80e3dcf5de23e7e015" + integrity sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA== + scheduler@^0.26.0: version "0.26.0" resolved "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz" @@ -3046,6 +3335,19 @@ space-separated-tokens@^2.0.0: resolved "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz" integrity sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q== +stats-gl@^2.2.8: + version "2.4.2" + resolved "https://registry.yarnpkg.com/stats-gl/-/stats-gl-2.4.2.tgz#28a6c869fc3a36a8be608ef21df63c0aad99d1ba" + integrity sha512-g5O9B0hm9CvnM36+v7SFl39T7hmAlv541tU81ME8YeSb3i1CIP5/QdDeSB3A0la0bKNHpxpwxOVRo2wFTYEosQ== + dependencies: + "@types/three" "*" + three "^0.170.0" + +stats.js@^0.17.0: + version "0.17.0" + resolved "https://registry.yarnpkg.com/stats.js/-/stats.js-0.17.0.tgz#b1c3dc46d94498b578b7fd3985b81ace7131cc7d" + integrity sha512-hNKz8phvYLPEcRkeG1rsGmV5ChMjKDAWU7/OJJdDErPBNChQXxCo3WZurGpnWc6gZhAzEPFad1aVgyOANH1sMw== + stringify-entities@^4.0.0: version "4.0.4" resolved "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz" @@ -3090,6 +3392,11 @@ supports-preserve-symlinks-flag@^1.0.0: resolved "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz" integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== +suspend-react@^0.1.3: + version "0.1.3" + resolved "https://registry.yarnpkg.com/suspend-react/-/suspend-react-0.1.3.tgz#a52f49d21cfae9a2fb70bd0c68413d3f9d90768e" + integrity sha512-aqldKgX9aZqpoDp3e8/BZ8Dm7x1pJl+qI3ZKxDN0i/IQTWUwBx/ManmlVJ3wowqbno6c2bmiIfs+Um6LbsjJyQ== + tailwindcss@4.1.8, tailwindcss@^4.1.8: version "4.1.8" resolved "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.8.tgz" @@ -3112,6 +3419,38 @@ tar@^7.4.3: mkdirp "^3.0.1" yallist "^5.0.0" +three-mesh-bvh@^0.8.3: + version "0.8.3" + resolved "https://registry.yarnpkg.com/three-mesh-bvh/-/three-mesh-bvh-0.8.3.tgz#c5e72472e7f062ff79084157a25c122d73184163" + integrity sha512-4G5lBaF+g2auKX3P0yqx+MJC6oVt6sB5k+CchS6Ob0qvH0YIhuUk1eYr7ktsIpY+albCqE80/FVQGV190PmiAg== + +three-stdlib@^2.35.6: + version "2.36.0" + resolved "https://registry.yarnpkg.com/three-stdlib/-/three-stdlib-2.36.0.tgz#1d806b8db9156a6c87ed10f61f56f8a3ab634b42" + integrity sha512-kv0Byb++AXztEGsULgMAs8U2jgUdz6HPpAB/wDJnLiLlaWQX2APHhiTJIN7rqW+Of0eRgcp7jn05U1BsCP3xBA== + dependencies: + "@types/draco3d" "^1.4.0" + "@types/offscreencanvas" "^2019.6.4" + "@types/webxr" "^0.5.2" + draco3d "^1.4.1" + fflate "^0.6.9" + potpack "^1.0.1" + +three@^0.170.0: + version "0.170.0" + resolved "https://registry.yarnpkg.com/three/-/three-0.170.0.tgz#6087f97aab79e9e9312f9c89fcef6808642dfbb7" + integrity sha512-FQK+LEpYc0fBD+J8g6oSEyyNzjp+Q7Ks1C568WWaoMRLW+TkNNWmenWeGgJjV105Gd+p/2ql1ZcjYvNiPZBhuQ== + +three@^0.175.0: + version "0.175.0" + resolved "https://registry.yarnpkg.com/three/-/three-0.175.0.tgz#67b357b0b1ee8ef0445b9a768f59363ab1fa7921" + integrity sha512-nNE3pnTHxXN/Phw768u0Grr7W4+rumGg/H6PgeseNJojkJtmeHJfZWi41Gp2mpXl1pg1pf1zjwR4McM1jTqkpg== + +three@^0.177.0: + version "0.177.0" + resolved "https://registry.yarnpkg.com/three/-/three-0.177.0.tgz#e51f2eb2b921fbab535bdfa81c403f9993b9dd83" + integrity sha512-EiXv5/qWAaGI+Vz2A+JfavwYCMdGjxVsrn3oBwllUoqYeaBO75J63ZfyaQKoiLrqNHoTlUc6PFgMXnS0kI45zg== + tinyglobby@^0.2.13: version "0.2.14" resolved "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz" @@ -3132,6 +3471,26 @@ trim-lines@^3.0.0: resolved "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz" integrity sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg== +troika-three-text@^0.52.4: + version "0.52.4" + resolved "https://registry.yarnpkg.com/troika-three-text/-/troika-three-text-0.52.4.tgz#f7b2389a2067d9506a5757457771cf4f6356e738" + integrity sha512-V50EwcYGruV5rUZ9F4aNsrytGdKcXKALjEtQXIOBfhVoZU9VAqZNIoGQ3TMiooVqFAbR1w15T+f+8gkzoFzawg== + dependencies: + bidi-js "^1.0.2" + troika-three-utils "^0.52.4" + troika-worker-utils "^0.52.0" + webgl-sdf-generator "1.1.1" + +troika-three-utils@^0.52.4: + version "0.52.4" + resolved "https://registry.yarnpkg.com/troika-three-utils/-/troika-three-utils-0.52.4.tgz#9292019e93cab97582af1cf491c4c895e5c03b66" + integrity sha512-NORAStSVa/BDiG52Mfudk4j1FG4jC4ILutB3foPnfGbOeIs9+G5vZLa0pnmnaftZUGm4UwSoqEpWdqvC7zms3A== + +troika-worker-utils@^0.52.0: + version "0.52.0" + resolved "https://registry.yarnpkg.com/troika-worker-utils/-/troika-worker-utils-0.52.0.tgz#ba5525fc444345006ebab0bc9cabdd66f1561e66" + integrity sha512-W1CpvTHykaPH5brv5VHLfQo9D1OYuo0cSBEUQFFT/nBUzM8iD6Lq2/tgG/f1OelbAS1WtaTPQzE5uM49egnngw== + trough@^2.0.0: version "2.2.0" resolved "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz" @@ -3147,6 +3506,13 @@ tslib@^2.4.0, tslib@^2.8.0: resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== +tunnel-rat@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/tunnel-rat/-/tunnel-rat-0.1.2.tgz#1717efbc474ea2d8aa05a91622457a6e201c0aeb" + integrity sha512-lR5VHmkPhzdhrM092lI2nACsLO4QubF0/yoOhzX7c+wIpbN1GjHNzCc91QlpxBi+cnx8vVJ+Ur6vL5cEoQPFpQ== + dependencies: + zustand "^4.3.2" + type-check@^0.4.0, type-check@~0.4.0: version "0.4.0" resolved "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz" @@ -3244,7 +3610,7 @@ uri-js@^4.2.2: dependencies: punycode "^2.1.0" -use-sync-external-store@^1.4.0: +use-sync-external-store@^1.2.2, use-sync-external-store@^1.4.0: version "1.5.0" resolved "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz" integrity sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A== @@ -3261,6 +3627,11 @@ util@^0.10.3: dependencies: inherits "2.0.3" +utility-types@^3.11.0: + version "3.11.0" + resolved "https://registry.yarnpkg.com/utility-types/-/utility-types-3.11.0.tgz#607c40edb4f258915e901ea7995607fdf319424c" + integrity sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw== + vfile-location@^5.0.0: version "5.0.3" resolved "https://registry.npmjs.org/vfile-location/-/vfile-location-5.0.3.tgz" @@ -3304,6 +3675,16 @@ web-namespaces@^2.0.0: resolved "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz" integrity sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ== +webgl-constants@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/webgl-constants/-/webgl-constants-1.1.1.tgz#f9633ee87fea56647a60b9ce735cbdfb891c6855" + integrity sha512-LkBXKjU5r9vAW7Gcu3T5u+5cvSvh5WwINdr0C+9jpzVB41cjQAP5ePArDtk/WHYdVj0GefCgM73BA7FlIiNtdg== + +webgl-sdf-generator@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/webgl-sdf-generator/-/webgl-sdf-generator-1.1.1.tgz#3e1b422b3d87cd3cc77f2602c9db63bc0f6accbd" + integrity sha512-9Z0JcMTFxeE+b2x1LJTdnaT8rT8aEp7MVxkNwoycNmJWwPdzoXzMh0BjJSh/AEFP+KPYZUli814h8bJZFIZ2jA== + which@^2.0.1: version "2.0.2" resolved "https://registry.npmjs.org/which/-/which-2.0.2.tgz" @@ -3336,6 +3717,18 @@ yocto-queue@^0.1.0: resolved "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz" integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== +zustand@^4.3.2: + version "4.5.7" + resolved "https://registry.yarnpkg.com/zustand/-/zustand-4.5.7.tgz#7d6bb2026a142415dd8be8891d7870e6dbe65f55" + integrity sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw== + dependencies: + use-sync-external-store "^1.2.2" + +zustand@^5.0.1, zustand@^5.0.3: + version "5.0.5" + resolved "https://registry.yarnpkg.com/zustand/-/zustand-5.0.5.tgz#3e236f6a953142d975336d179bc735d97db17e84" + integrity sha512-mILtRfKW9xM47hqxGIxCv12gXusoY/xTSHBYApXozR0HmQv299whhBeeAcRy+KrPPybzosvJBCOmVjq6x12fCg== + zwitch@^2.0.0: version "2.0.4" resolved "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz"