Compare commits
	
		
			8 Commits
		
	
	
		
			a357994025
			...
			main
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 50ad374cf5 | |||
| 9e47ab667f | |||
| 1b8fc3d215 | |||
| f5142ec95d | |||
| cdb96dfb8b | |||
| c50ccb3a0c | |||
| 4bcc2e2cca | |||
| 26e4d70b95 | 
							
								
								
									
										1
									
								
								.env
									
									
									
									
									
								
							
							
						
						
									
										1
									
								
								.env
									
									
									
									
									
								
							| @@ -1,2 +1,3 @@ | |||||||
|  | VITE_API_URL='https://wn.krbl.ru' | ||||||
| VITE_REACT_APP ='https://wn.krbl.ru/' | VITE_REACT_APP ='https://wn.krbl.ru/' | ||||||
| VITE_KRBL_MEDIA='https://wn.krbl.ru/media/' | VITE_KRBL_MEDIA='https://wn.krbl.ru/media/' | ||||||
							
								
								
									
										158
									
								
								src/app/GlobalErrorBoundary.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										158
									
								
								src/app/GlobalErrorBoundary.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,158 @@ | |||||||
|  | import React, { Component, ReactNode } from "react"; | ||||||
|  | import { Box, Button, Typography, Paper, Container } from "@mui/material"; | ||||||
|  | import { RefreshCw, Home, AlertTriangle } from "lucide-react"; | ||||||
|  |  | ||||||
|  | interface Props { | ||||||
|  |   children: ReactNode; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | interface State { | ||||||
|  |   hasError: boolean; | ||||||
|  |   error: Error | null; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export class GlobalErrorBoundary extends Component<Props, State> { | ||||||
|  |   constructor(props: Props) { | ||||||
|  |     super(props); | ||||||
|  |     this.state = { | ||||||
|  |       hasError: false, | ||||||
|  |       error: null, | ||||||
|  |     }; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   static getDerivedStateFromError(error: Error): State { | ||||||
|  |     return { | ||||||
|  |       hasError: true, | ||||||
|  |       error, | ||||||
|  |     }; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   componentDidCatch(error: Error, errorInfo: React.ErrorInfo) { | ||||||
|  |     console.error("❌ GlobalErrorBoundary: Критическая ошибка приложения", { | ||||||
|  |       error: error.message, | ||||||
|  |       stack: error.stack, | ||||||
|  |       componentStack: errorInfo.componentStack, | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   handleReset = () => { | ||||||
|  |     this.setState({ | ||||||
|  |       hasError: false, | ||||||
|  |       error: null, | ||||||
|  |     }); | ||||||
|  |     window.location.reload(); | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   handleGoHome = () => { | ||||||
|  |     this.setState({ | ||||||
|  |       hasError: false, | ||||||
|  |       error: null, | ||||||
|  |     }); | ||||||
|  |     window.location.href = "/"; | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   render() { | ||||||
|  |     if (this.state.hasError) { | ||||||
|  |       return ( | ||||||
|  |         <Box | ||||||
|  |           sx={{ | ||||||
|  |             minHeight: "100vh", | ||||||
|  |             display: "flex", | ||||||
|  |             justifyContent: "center", | ||||||
|  |             alignItems: "center", | ||||||
|  |             backgroundColor: "background.default", | ||||||
|  |             p: 2, | ||||||
|  |           }} | ||||||
|  |         > | ||||||
|  |           <Container maxWidth="sm"> | ||||||
|  |             <Paper | ||||||
|  |               elevation={3} | ||||||
|  |               sx={{ | ||||||
|  |                 p: 4, | ||||||
|  |                 textAlign: "center", | ||||||
|  |               }} | ||||||
|  |             > | ||||||
|  |               <Box | ||||||
|  |                 sx={{ | ||||||
|  |                   display: "flex", | ||||||
|  |                   justifyContent: "center", | ||||||
|  |                   mb: 3, | ||||||
|  |                 }} | ||||||
|  |               > | ||||||
|  |                 <AlertTriangle size={64} color="#f44336" /> | ||||||
|  |               </Box> | ||||||
|  |  | ||||||
|  |               <Typography variant="h4" component="h1" gutterBottom> | ||||||
|  |                 Упс! Что-то пошло не так | ||||||
|  |               </Typography> | ||||||
|  |  | ||||||
|  |               <Typography variant="body1" color="text.secondary" sx={{ mb: 3 }}> | ||||||
|  |                 Приложение столкнулось с неожиданной ошибкой. Попробуйте | ||||||
|  |                 перезагрузить страницу или вернуться на главную. | ||||||
|  |               </Typography> | ||||||
|  |  | ||||||
|  |               {this.state.error?.message && ( | ||||||
|  |                 <Paper | ||||||
|  |                   variant="outlined" | ||||||
|  |                   sx={{ | ||||||
|  |                     p: 2, | ||||||
|  |                     mb: 3, | ||||||
|  |                     backgroundColor: "error.light", | ||||||
|  |                     color: "error.contrastText", | ||||||
|  |                     textAlign: "left", | ||||||
|  |                   }} | ||||||
|  |                 > | ||||||
|  |                   <Typography | ||||||
|  |                     variant="caption" | ||||||
|  |                     sx={{ fontWeight: "bold", display: "block", mb: 1 }} | ||||||
|  |                   > | ||||||
|  |                     Информация об ошибке: | ||||||
|  |                   </Typography> | ||||||
|  |                   <Typography | ||||||
|  |                     variant="caption" | ||||||
|  |                     sx={{ | ||||||
|  |                       fontFamily: "monospace", | ||||||
|  |                       fontSize: "0.75rem", | ||||||
|  |                       wordBreak: "break-word", | ||||||
|  |                       display: "block", | ||||||
|  |                     }} | ||||||
|  |                   > | ||||||
|  |                     {this.state.error.message} | ||||||
|  |                   </Typography> | ||||||
|  |                 </Paper> | ||||||
|  |               )} | ||||||
|  |  | ||||||
|  |               <Box | ||||||
|  |                 sx={{ | ||||||
|  |                   display: "flex", | ||||||
|  |                   gap: 2, | ||||||
|  |                   justifyContent: "center", | ||||||
|  |                   flexWrap: "wrap", | ||||||
|  |                 }} | ||||||
|  |               > | ||||||
|  |                 <Button | ||||||
|  |                   variant="outlined" | ||||||
|  |                   startIcon={<Home size={16} />} | ||||||
|  |                   onClick={this.handleGoHome} | ||||||
|  |                   size="large" | ||||||
|  |                 > | ||||||
|  |                   На главную | ||||||
|  |                 </Button> | ||||||
|  |                 <Button | ||||||
|  |                   variant="contained" | ||||||
|  |                   startIcon={<RefreshCw size={16} />} | ||||||
|  |                   onClick={this.handleReset} | ||||||
|  |                   size="large" | ||||||
|  |                 > | ||||||
|  |                   Перезагрузить | ||||||
|  |                 </Button> | ||||||
|  |               </Box> | ||||||
|  |             </Paper> | ||||||
|  |           </Container> | ||||||
|  |         </Box> | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return this.props.children; | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -4,10 +4,13 @@ import { Router } from "./router"; | |||||||
| import { CustomTheme } from "@shared"; | import { CustomTheme } from "@shared"; | ||||||
| import { ThemeProvider } from "@mui/material/styles"; | import { ThemeProvider } from "@mui/material/styles"; | ||||||
| import { ToastContainer } from "react-toastify"; | import { ToastContainer } from "react-toastify"; | ||||||
|  | import { GlobalErrorBoundary } from "./GlobalErrorBoundary"; | ||||||
|  |  | ||||||
| export const App: React.FC = () => ( | export const App: React.FC = () => ( | ||||||
|   <ThemeProvider theme={CustomTheme.Light}> |   <GlobalErrorBoundary> | ||||||
|     <ToastContainer /> |     <ThemeProvider theme={CustomTheme.Light}> | ||||||
|     <Router /> |       <ToastContainer /> | ||||||
|   </ThemeProvider> |       <Router /> | ||||||
|  |     </ThemeProvider> | ||||||
|  |   </GlobalErrorBoundary> | ||||||
| ); | ); | ||||||
|   | |||||||
| @@ -5,6 +5,7 @@ export interface NavigationItem { | |||||||
|   label: string; |   label: string; | ||||||
|   icon: LucideIcon; |   icon: LucideIcon; | ||||||
|   path?: string; |   path?: string; | ||||||
|  |   for_admin?: boolean; | ||||||
|   onClick?: () => void; |   onClick?: () => void; | ||||||
|   nestedItems?: NavigationItem[]; |   nestedItems?: NavigationItem[]; | ||||||
| } | } | ||||||
|   | |||||||
| @@ -10,6 +10,7 @@ import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; | |||||||
| import type { NavigationItem } from "../model"; | import type { NavigationItem } from "../model"; | ||||||
| import { useNavigate, useLocation } from "react-router-dom"; | import { useNavigate, useLocation } from "react-router-dom"; | ||||||
| import { Plus } from "lucide-react"; | import { Plus } from "lucide-react"; | ||||||
|  | import { authStore } from "@shared"; | ||||||
|  |  | ||||||
| interface NavigationItemProps { | interface NavigationItemProps { | ||||||
|   item: NavigationItem; |   item: NavigationItem; | ||||||
| @@ -30,9 +31,21 @@ export const NavigationItemComponent: React.FC<NavigationItemProps> = ({ | |||||||
|   const navigate = useNavigate(); |   const navigate = useNavigate(); | ||||||
|   const location = useLocation(); |   const location = useLocation(); | ||||||
|   const [isExpanded, setIsExpanded] = React.useState(false); |   const [isExpanded, setIsExpanded] = React.useState(false); | ||||||
|  |   const { payload } = authStore; | ||||||
|  |  | ||||||
|  |   // @ts-ignore | ||||||
|  |   const isAdmin = payload?.is_admin || false; | ||||||
|  |  | ||||||
|   const isActive = item.path ? location.pathname.startsWith(item.path) : false; |   const isActive = item.path ? location.pathname.startsWith(item.path) : false; | ||||||
|  |  | ||||||
|  |   const filteredNestedItems = item.nestedItems?.filter((nestedItem) => { | ||||||
|  |     if (nestedItem.for_admin) { | ||||||
|  |       return isAdmin; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return true; | ||||||
|  |   }); | ||||||
|  |  | ||||||
|   const handleClick = () => { |   const handleClick = () => { | ||||||
|     if (item.id === "all" && !open) { |     if (item.id === "all" && !open) { | ||||||
|       onDrawerOpen?.(); |       onDrawerOpen?.(); | ||||||
| @@ -108,15 +121,16 @@ export const NavigationItemComponent: React.FC<NavigationItemProps> = ({ | |||||||
|               }, |               }, | ||||||
|             ]} |             ]} | ||||||
|           /> |           /> | ||||||
|           {item.nestedItems && |           {filteredNestedItems && | ||||||
|  |             filteredNestedItems.length > 0 && | ||||||
|             open && |             open && | ||||||
|             (isExpanded ? <ExpandLessIcon /> : <ExpandMoreIcon />)} |             (isExpanded ? <ExpandLessIcon /> : <ExpandMoreIcon />)} | ||||||
|         </ListItemButton> |         </ListItemButton> | ||||||
|       </ListItem> |       </ListItem> | ||||||
|       {item.nestedItems && ( |       {filteredNestedItems && filteredNestedItems.length > 0 && ( | ||||||
|         <Collapse in={isExpanded} timeout="auto" unmountOnExit> |         <Collapse in={isExpanded} timeout="auto" unmountOnExit> | ||||||
|           <List component="div" disablePadding> |           <List component="div" disablePadding> | ||||||
|             {item.nestedItems.map((nestedItem) => ( |             {filteredNestedItems.map((nestedItem) => ( | ||||||
|               <NavigationItemComponent |               <NavigationItemComponent | ||||||
|                 key={nestedItem.id} |                 key={nestedItem.id} | ||||||
|                 item={nestedItem} |                 item={nestedItem} | ||||||
|   | |||||||
| @@ -1,41 +1,62 @@ | |||||||
| import List from "@mui/material/List"; | import List from "@mui/material/List"; | ||||||
| import Divider from "@mui/material/Divider"; | import Divider from "@mui/material/Divider"; | ||||||
| import { NAVIGATION_ITEMS } from "@shared"; | import { authStore, NAVIGATION_ITEMS } from "@shared"; | ||||||
| import { NavigationItem, NavigationItemComponent } from "@entities"; | import { NavigationItem, NavigationItemComponent } from "@entities"; | ||||||
|  | import { observer } from "mobx-react-lite"; | ||||||
|  |  | ||||||
| interface NavigationListProps { | interface NavigationListProps { | ||||||
|   open: boolean; |   open: boolean; | ||||||
|   onDrawerOpen?: () => void; |   onDrawerOpen?: () => void; | ||||||
| } | } | ||||||
|  |  | ||||||
| export const NavigationList = ({ open, onDrawerOpen }: NavigationListProps) => { | export const NavigationList = observer( | ||||||
|   const primaryItems = NAVIGATION_ITEMS.primary; |   ({ open, onDrawerOpen }: NavigationListProps) => { | ||||||
|   const secondaryItems = NAVIGATION_ITEMS.secondary; |     const { payload } = authStore; | ||||||
|  |     // @ts-ignore | ||||||
|  |     const isAdmin = Boolean(payload?.is_admin) || false; | ||||||
|  |  | ||||||
|   return ( |     const primaryItems = NAVIGATION_ITEMS.primary.filter((item) => { | ||||||
|     <> |       if (item.for_admin) { | ||||||
|       <List> |         return isAdmin; | ||||||
|         {primaryItems.map((item) => ( |       } | ||||||
|           <NavigationItemComponent |  | ||||||
|             key={item.id} |       if (item.nestedItems && item.nestedItems.length > 0) { | ||||||
|             item={item as NavigationItem} |         return item.nestedItems.some((nestedItem) => { | ||||||
|             open={open} |           if (nestedItem.for_admin) { | ||||||
|             onDrawerOpen={onDrawerOpen} |             return isAdmin; | ||||||
|           /> |           } | ||||||
|         ))} |           return true; | ||||||
|       </List> |         }); | ||||||
|       <Divider /> |       } | ||||||
|       <List> |  | ||||||
|         {secondaryItems.map((item) => ( |       return true; | ||||||
|           <NavigationItemComponent |     }); | ||||||
|             key={item.id} |  | ||||||
|             item={item as NavigationItem} |     return ( | ||||||
|             open={open} |       <> | ||||||
|             onClick={item.onClick ? item.onClick : undefined} |         <List> | ||||||
|             onDrawerOpen={onDrawerOpen} |           {primaryItems.map((item) => ( | ||||||
|           /> |             <NavigationItemComponent | ||||||
|         ))} |               key={item.id} | ||||||
|       </List> |               item={item as NavigationItem} | ||||||
|     </> |               open={open} | ||||||
|   ); |               onDrawerOpen={onDrawerOpen} | ||||||
| }; |             /> | ||||||
|  |           ))} | ||||||
|  |         </List> | ||||||
|  |         <Divider /> | ||||||
|  |         <List> | ||||||
|  |           {NAVIGATION_ITEMS.secondary.map((item) => ( | ||||||
|  |             <NavigationItemComponent | ||||||
|  |               key={item.id} | ||||||
|  |               item={item as NavigationItem} | ||||||
|  |               open={open} | ||||||
|  |               onClick={item.onClick ? item.onClick : undefined} | ||||||
|  |               onDrawerOpen={onDrawerOpen} | ||||||
|  |             /> | ||||||
|  |           ))} | ||||||
|  |         </List> | ||||||
|  |       </> | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | ); | ||||||
|   | |||||||
| @@ -46,6 +46,7 @@ export const ArticleListPage = observer(() => { | |||||||
|     { |     { | ||||||
|       field: "actions", |       field: "actions", | ||||||
|       headerName: "Действия", |       headerName: "Действия", | ||||||
|  |       sortable: false, | ||||||
|  |  | ||||||
|       renderCell: (params: GridRenderCellParams) => { |       renderCell: (params: GridRenderCellParams) => { | ||||||
|         return ( |         return ( | ||||||
|   | |||||||
| @@ -88,6 +88,7 @@ export const CarrierListPage = observer(() => { | |||||||
|       headerName: "Действия", |       headerName: "Действия", | ||||||
|       headerAlign: "center", |       headerAlign: "center", | ||||||
|       width: 200, |       width: 200, | ||||||
|  |       sortable: false, | ||||||
|  |  | ||||||
|       renderCell: (params: GridRenderCellParams) => { |       renderCell: (params: GridRenderCellParams) => { | ||||||
|         return ( |         return ( | ||||||
|   | |||||||
| @@ -93,6 +93,7 @@ export const CityListPage = observer(() => { | |||||||
|       align: "center", |       align: "center", | ||||||
|       headerAlign: "center", |       headerAlign: "center", | ||||||
|       width: 200, |       width: 200, | ||||||
|  |       sortable: false, | ||||||
|       renderCell: (params: GridRenderCellParams) => { |       renderCell: (params: GridRenderCellParams) => { | ||||||
|         return ( |         return ( | ||||||
|           <div className="flex h-full gap-7 justify-center items-center"> |           <div className="flex h-full gap-7 justify-center items-center"> | ||||||
|   | |||||||
| @@ -50,6 +50,7 @@ export const CountryListPage = observer(() => { | |||||||
|       align: "center", |       align: "center", | ||||||
|       headerAlign: "center", |       headerAlign: "center", | ||||||
|       width: 200, |       width: 200, | ||||||
|  |       sortable: false, | ||||||
|       renderCell: (params: GridRenderCellParams) => { |       renderCell: (params: GridRenderCellParams) => { | ||||||
|         return ( |         return ( | ||||||
|           <div className="flex h-full gap-7 justify-center items-center"> |           <div className="flex h-full gap-7 justify-center items-center"> | ||||||
|   | |||||||
| @@ -52,7 +52,12 @@ export const LoginPage = () => { | |||||||
|       } |       } | ||||||
|  |  | ||||||
|       navigate("/map"); |       navigate("/map"); | ||||||
|       await getUsers(); |       try { | ||||||
|  |         await getUsers(); | ||||||
|  |       } catch (err) { | ||||||
|  |         console.error(err); | ||||||
|  |       } | ||||||
|  |  | ||||||
|       toast.success("Вход в систему выполнен успешно"); |       toast.success("Вход в систему выполнен успешно"); | ||||||
|     } catch (err) { |     } catch (err) { | ||||||
|       setError( |       setError( | ||||||
|   | |||||||
| @@ -16,7 +16,12 @@ import { | |||||||
|   Draw, |   Draw, | ||||||
|   Modify, |   Modify, | ||||||
|   Select, |   Select, | ||||||
|   defaults as defaultInteractions, |   DragPan, | ||||||
|  |   MouseWheelZoom, | ||||||
|  |   KeyboardPan, | ||||||
|  |   KeyboardZoom, | ||||||
|  |   PinchZoom, | ||||||
|  |   PinchRotate, | ||||||
| } from "ol/interaction"; | } from "ol/interaction"; | ||||||
| import { DrawEvent } from "ol/interaction/Draw"; | import { DrawEvent } from "ol/interaction/Draw"; | ||||||
| import { SelectEvent } from "ol/interaction/Select"; | import { SelectEvent } from "ol/interaction/Select"; | ||||||
| @@ -102,6 +107,7 @@ import { | |||||||
|   sightsStore, |   sightsStore, | ||||||
|   menuStore, |   menuStore, | ||||||
|   selectedCityStore, |   selectedCityStore, | ||||||
|  |   carrierStore, | ||||||
| } from "@shared"; | } from "@shared"; | ||||||
|  |  | ||||||
| // Функция для сброса кешей карты | // Функция для сброса кешей карты | ||||||
| @@ -123,6 +129,7 @@ interface ApiRoute { | |||||||
|   path: [number, number][]; |   path: [number, number][]; | ||||||
|   center_latitude: number; |   center_latitude: number; | ||||||
|   center_longitude: number; |   center_longitude: number; | ||||||
|  |   carrier_id: number; | ||||||
| } | } | ||||||
|  |  | ||||||
| interface ApiStation { | interface ApiStation { | ||||||
| @@ -132,6 +139,7 @@ interface ApiStation { | |||||||
|   longitude: number; |   longitude: number; | ||||||
|   city_id: number; |   city_id: number; | ||||||
|   created_at?: string; |   created_at?: string; | ||||||
|  |   updated_at?: string; | ||||||
| } | } | ||||||
|  |  | ||||||
| interface ApiSight { | interface ApiSight { | ||||||
| @@ -142,9 +150,16 @@ interface ApiSight { | |||||||
|   longitude: number; |   longitude: number; | ||||||
|   city_id: number; |   city_id: number; | ||||||
|   created_at?: string; |   created_at?: string; | ||||||
|  |   updated_at?: string; | ||||||
| } | } | ||||||
|  |  | ||||||
| export type SortType = "name_asc" | "name_desc" | "date_asc" | "date_desc"; | export type SortType = | ||||||
|  |   | "name_asc" | ||||||
|  |   | "name_desc" | ||||||
|  |   | "created_asc" | ||||||
|  |   | "created_desc" | ||||||
|  |   | "updated_asc" | ||||||
|  |   | "updated_desc"; | ||||||
|  |  | ||||||
| class MapStore { | class MapStore { | ||||||
|   constructor() { |   constructor() { | ||||||
| @@ -179,7 +194,7 @@ class MapStore { | |||||||
|         return sorted.sort((a, b) => a.name.localeCompare(b.name)); |         return sorted.sort((a, b) => a.name.localeCompare(b.name)); | ||||||
|       case "name_desc": |       case "name_desc": | ||||||
|         return sorted.sort((a, b) => b.name.localeCompare(a.name)); |         return sorted.sort((a, b) => b.name.localeCompare(a.name)); | ||||||
|       case "date_asc": |       case "created_asc": | ||||||
|         return sorted.sort((a, b) => { |         return sorted.sort((a, b) => { | ||||||
|           if ( |           if ( | ||||||
|             "created_at" in a && |             "created_at" in a && | ||||||
| @@ -195,7 +210,7 @@ class MapStore { | |||||||
|           // Фоллбэк: сортировка по ID, если дата недоступна |           // Фоллбэк: сортировка по ID, если дата недоступна | ||||||
|           return a.id - b.id; |           return a.id - b.id; | ||||||
|         }); |         }); | ||||||
|       case "date_desc": |       case "created_desc": | ||||||
|         return sorted.sort((a, b) => { |         return sorted.sort((a, b) => { | ||||||
|           if ( |           if ( | ||||||
|             "created_at" in a && |             "created_at" in a && | ||||||
| @@ -211,6 +226,32 @@ class MapStore { | |||||||
|           // Фоллбэк: сортировка по ID, если дата недоступна |           // Фоллбэк: сортировка по ID, если дата недоступна | ||||||
|           return b.id - a.id; |           return b.id - a.id; | ||||||
|         }); |         }); | ||||||
|  |       case "updated_asc": | ||||||
|  |         return sorted.sort((a, b) => { | ||||||
|  |           const aUpdated = | ||||||
|  |             ("updated_at" in a && a.updated_at) || | ||||||
|  |             ("created_at" in a && a.created_at); | ||||||
|  |           const bUpdated = | ||||||
|  |             ("updated_at" in b && b.updated_at) || | ||||||
|  |             ("created_at" in b && b.created_at); | ||||||
|  |           if (typeof aUpdated === "string" && typeof bUpdated === "string") { | ||||||
|  |             return new Date(aUpdated).getTime() - new Date(bUpdated).getTime(); | ||||||
|  |           } | ||||||
|  |           return a.id - b.id; | ||||||
|  |         }); | ||||||
|  |       case "updated_desc": | ||||||
|  |         return sorted.sort((a, b) => { | ||||||
|  |           const aUpdated = | ||||||
|  |             ("updated_at" in a && a.updated_at) || | ||||||
|  |             ("created_at" in a && a.created_at); | ||||||
|  |           const bUpdated = | ||||||
|  |             ("updated_at" in b && b.updated_at) || | ||||||
|  |             ("created_at" in b && b.created_at); | ||||||
|  |           if (typeof aUpdated === "string" && typeof bUpdated === "string") { | ||||||
|  |             return new Date(bUpdated).getTime() - new Date(aUpdated).getTime(); | ||||||
|  |           } | ||||||
|  |           return b.id - a.id; | ||||||
|  |         }); | ||||||
|       default: |       default: | ||||||
|         return sorted; |         return sorted; | ||||||
|     } |     } | ||||||
| @@ -236,6 +277,23 @@ class MapStore { | |||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   get filteredRoutes(): ApiRoute[] { | ||||||
|  |     const selectedCityId = selectedCityStore.selectedCityId; | ||||||
|  |     if (!selectedCityId) { | ||||||
|  |       return this.routes; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // Получаем carriers для текущего языка | ||||||
|  |     const carriers = carrierStore.carriers.ru.data; | ||||||
|  |  | ||||||
|  |     // Фильтруем маршруты по городу через carriers | ||||||
|  |     return this.routes.filter((route: ApiRoute) => { | ||||||
|  |       // Находим carrier для маршрута | ||||||
|  |       const carrier = carriers.find((c: any) => c.id === route.carrier_id); | ||||||
|  |       return carrier && carrier.city_id === selectedCityId; | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|   get filteredSights(): ApiSight[] { |   get filteredSights(): ApiSight[] { | ||||||
|     const selectedCityId = selectedCityStore.selectedCityId; |     const selectedCityId = selectedCityStore.selectedCityId; | ||||||
|     if (!selectedCityId) { |     if (!selectedCityId) { | ||||||
| @@ -253,7 +311,14 @@ class MapStore { | |||||||
|       languageInstance("ru").get(`/route/${id}`) |       languageInstance("ru").get(`/route/${id}`) | ||||||
|     ); |     ); | ||||||
|     const routeResponses = await Promise.all(routePromises); |     const routeResponses = await Promise.all(routePromises); | ||||||
|     this.routes = routeResponses.map((res) => res.data); |     this.routes = routeResponses.map((res) => ({ | ||||||
|  |       id: res.data.id, | ||||||
|  |       route_number: res.data.route_number, | ||||||
|  |       path: res.data.path, | ||||||
|  |       center_latitude: res.data.center_latitude, | ||||||
|  |       center_longitude: res.data.center_longitude, | ||||||
|  |       carrier_id: res.data.carrier_id, | ||||||
|  |     })); | ||||||
|  |  | ||||||
|     this.routes = this.routes.sort((a, b) => |     this.routes = this.routes.sort((a, b) => | ||||||
|       a.route_number.localeCompare(b.route_number) |       a.route_number.localeCompare(b.route_number) | ||||||
| @@ -338,13 +403,28 @@ class MapStore { | |||||||
|         "EPSG:3857" |         "EPSG:3857" | ||||||
|       ); |       ); | ||||||
|  |  | ||||||
|  |       // Автоматически назначаем перевозчика из выбранного города | ||||||
|  |       let carrier_id = 0; | ||||||
|  |       let carrier = ""; | ||||||
|  |  | ||||||
|  |       if (selectedCityStore.selectedCityId) { | ||||||
|  |         const carriersInCity = carrierStore.carriers.ru.data.filter( | ||||||
|  |           (c: any) => c.city_id === selectedCityStore.selectedCityId | ||||||
|  |         ); | ||||||
|  |  | ||||||
|  |         if (carriersInCity.length > 0) { | ||||||
|  |           carrier_id = carriersInCity[0].id; | ||||||
|  |           carrier = carriersInCity[0].full_name; | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |  | ||||||
|       const routeData = { |       const routeData = { | ||||||
|         route_number, |         route_number, | ||||||
|         path, |         path, | ||||||
|         center_latitude, |         center_latitude, | ||||||
|         center_longitude, |         center_longitude, | ||||||
|         carrier: "", |         carrier, | ||||||
|         carrier_id: 0, |         carrier_id, | ||||||
|         governor_appeal: 0, |         governor_appeal: 0, | ||||||
|         rotate: 0, |         rotate: 0, | ||||||
|         route_direction: false, |         route_direction: false, | ||||||
| @@ -354,6 +434,12 @@ class MapStore { | |||||||
|       }; |       }; | ||||||
|  |  | ||||||
|       await routeStore.createRoute(routeData); |       await routeStore.createRoute(routeData); | ||||||
|  |  | ||||||
|  |       if (!carrier_id && selectedCityStore.selectedCityId) { | ||||||
|  |         toast.error( | ||||||
|  |           "В выбранном городе нет доступных перевозчиков, маршрут отображается в общем списке" | ||||||
|  |         ); | ||||||
|  |       } | ||||||
|       createdItem = routeStore.routes.data[routeStore.routes.data.length - 1]; |       createdItem = routeStore.routes.data[routeStore.routes.data.length - 1]; | ||||||
|     } else if (featureType === "sight") { |     } else if (featureType === "sight") { | ||||||
|       const name = properties.name || "Достопримечательность 1"; |       const name = properties.name || "Достопримечательность 1"; | ||||||
| @@ -595,10 +681,6 @@ interface MapServiceConfig { | |||||||
|   zoom: number; |   zoom: number; | ||||||
| } | } | ||||||
|  |  | ||||||
| interface HistoryState { |  | ||||||
|   state: string; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| type FeatureType = "station" | "route" | "sight"; | type FeatureType = "station" | "route" | "sight"; | ||||||
|  |  | ||||||
| class MapService { | class MapService { | ||||||
| @@ -620,9 +702,6 @@ class MapService { | |||||||
|   private modifyInteraction: Modify; |   private modifyInteraction: Modify; | ||||||
|   private selectInteraction: Select; |   private selectInteraction: Select; | ||||||
|   private hoveredFeatureId: string | number | null; |   private hoveredFeatureId: string | number | null; | ||||||
|   private history: HistoryState[]; |  | ||||||
|   private historyIndex: number; |  | ||||||
|   private beforeActionState: string | null = null; |  | ||||||
|   private boundHandlePointerMove: ( |   private boundHandlePointerMove: ( | ||||||
|     event: MapBrowserEvent<PointerEvent> |     event: MapBrowserEvent<PointerEvent> | ||||||
|   ) => void; |   ) => void; | ||||||
| @@ -633,6 +712,7 @@ class MapService { | |||||||
|   private selectedIds: Set<string | number> = new Set(); |   private selectedIds: Set<string | number> = new Set(); | ||||||
|   private onSelectionChange: ((ids: Set<string | number>) => void) | null = |   private onSelectionChange: ((ids: Set<string | number>) => void) | null = | ||||||
|     null; |     null; | ||||||
|  |   private isCreating: boolean = false; | ||||||
|  |  | ||||||
|   // Styles |   // Styles | ||||||
|   private defaultStyle: Style; |   private defaultStyle: Style; | ||||||
| @@ -674,8 +754,6 @@ class MapService { | |||||||
|     this.currentDrawingFeatureType = null; |     this.currentDrawingFeatureType = null; | ||||||
|     this.currentInteraction = null; |     this.currentInteraction = null; | ||||||
|     this.hoveredFeatureId = null; |     this.hoveredFeatureId = null; | ||||||
|     this.history = []; |  | ||||||
|     this.historyIndex = -1; |  | ||||||
|     this.clusterStyleCache = {}; |     this.clusterStyleCache = {}; | ||||||
|  |  | ||||||
|     this.setLoading = setLoading; |     this.setLoading = setLoading; | ||||||
| @@ -909,7 +987,33 @@ class MapService { | |||||||
|           center: transform(initialCenter, "EPSG:4326", "EPSG:3857"), |           center: transform(initialCenter, "EPSG:4326", "EPSG:3857"), | ||||||
|           zoom: initialZoom, |           zoom: initialZoom, | ||||||
|         }), |         }), | ||||||
|         interactions: defaultInteractions({ doubleClickZoom: false }), |         interactions: [ | ||||||
|  |           new MouseWheelZoom(), | ||||||
|  |           new KeyboardPan(), | ||||||
|  |           new KeyboardZoom(), | ||||||
|  |           new PinchZoom(), | ||||||
|  |           new PinchRotate(), | ||||||
|  |           // Отключаем DoubleClickZoom как было изначально | ||||||
|  |           // new DoubleClickZoom(), | ||||||
|  |           new DragPan({ | ||||||
|  |             condition: (event) => { | ||||||
|  |               // Разрешаем перетаскивание только при нажатии средней кнопки мыши (колёсико) | ||||||
|  |               const originalEvent = event.originalEvent; | ||||||
|  |               if (!originalEvent) return false; | ||||||
|  |  | ||||||
|  |               // Проверяем, что это событие мыши и нажата средняя кнопка | ||||||
|  |               if ( | ||||||
|  |                 originalEvent.type === "pointerdown" || | ||||||
|  |                 originalEvent.type === "pointermove" | ||||||
|  |               ) { | ||||||
|  |                 const pointerEvent = originalEvent as PointerEvent; | ||||||
|  |                 return pointerEvent.buttons === 4; // 4 = средняя кнопка мыши | ||||||
|  |               } | ||||||
|  |  | ||||||
|  |               return false; | ||||||
|  |             }, | ||||||
|  |           }), | ||||||
|  |         ], | ||||||
|         controls: [], |         controls: [], | ||||||
|       }); |       }); | ||||||
|  |  | ||||||
| @@ -1037,21 +1141,10 @@ class MapService { | |||||||
|       }, |       }, | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     // @ts-ignore |  | ||||||
|     this.modifyInteraction.on("modifystart", () => { |  | ||||||
|       if (!this.beforeActionState) { |  | ||||||
|         this.beforeActionState = this.getCurrentStateAsGeoJSON(); |  | ||||||
|       } |  | ||||||
|     }); |  | ||||||
|  |  | ||||||
|     this.modifyInteraction.on("modifyend", (event) => { |     this.modifyInteraction.on("modifyend", (event) => { | ||||||
|       if (this.beforeActionState) { |  | ||||||
|         this.addStateToHistory(this.beforeActionState); |  | ||||||
|       } |  | ||||||
|       event.features.getArray().forEach((feature) => { |       event.features.getArray().forEach((feature) => { | ||||||
|         this.saveModifiedFeature(feature as Feature<Geometry>); |         this.saveModifiedFeature(feature as Feature<Geometry>); | ||||||
|       }); |       }); | ||||||
|       this.beforeActionState = null; |  | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     if (this.map) { |     if (this.map) { | ||||||
| @@ -1100,11 +1193,6 @@ class MapService { | |||||||
|           return; |           return; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         const beforeState = this.getCurrentStateAsGeoJSON(); |  | ||||||
|         if (beforeState) { |  | ||||||
|           this.addStateToHistory(beforeState); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         const newCoordinates = coordinates.filter( |         const newCoordinates = coordinates.filter( | ||||||
|           (_, index) => index !== closestIndex |           (_, index) => index !== closestIndex | ||||||
|         ); |         ); | ||||||
| @@ -1239,8 +1327,52 @@ class MapService { | |||||||
|       this.map.on("pointermove", this.boundHandlePointerMove as any); |       this.map.on("pointermove", this.boundHandlePointerMove as any); | ||||||
|       const targetEl = this.map.getTargetElement(); |       const targetEl = this.map.getTargetElement(); | ||||||
|       if (targetEl instanceof HTMLElement) { |       if (targetEl instanceof HTMLElement) { | ||||||
|  |         // Устанавливаем курсор pointer по умолчанию для всей карты | ||||||
|  |         targetEl.style.cursor = "pointer"; | ||||||
|         targetEl.addEventListener("contextmenu", this.boundHandleContextMenu); |         targetEl.addEventListener("contextmenu", this.boundHandleContextMenu); | ||||||
|         targetEl.addEventListener("pointerleave", this.boundHandlePointerLeave); |         targetEl.addEventListener("pointerleave", this.boundHandlePointerLeave); | ||||||
|  |  | ||||||
|  |         // Добавляем обработчики для изменения курсора при нажатии средней кнопки мыши | ||||||
|  |         targetEl.addEventListener("pointerdown", (e) => { | ||||||
|  |           if (e.buttons === 4) { | ||||||
|  |             // Средняя кнопка мыши | ||||||
|  |             e.preventDefault(); // Предотвращаем скролл страницы | ||||||
|  |             targetEl.style.cursor = "grabbing"; | ||||||
|  |           } | ||||||
|  |         }); | ||||||
|  |  | ||||||
|  |         targetEl.addEventListener("pointerup", (e) => { | ||||||
|  |           if (e.button === 1) { | ||||||
|  |             // Средняя кнопка мыши отпущена | ||||||
|  |             e.preventDefault(); // Предотвращаем скролл страницы | ||||||
|  |             targetEl.style.cursor = "pointer"; | ||||||
|  |           } | ||||||
|  |         }); | ||||||
|  |  | ||||||
|  |         // Также добавляем обработчик для mousedown/mouseup для совместимости | ||||||
|  |         targetEl.addEventListener("mousedown", (e) => { | ||||||
|  |           if (e.button === 1) { | ||||||
|  |             // Средняя кнопка мыши | ||||||
|  |             e.preventDefault(); // Предотвращаем скролл страницы | ||||||
|  |             targetEl.style.cursor = "grabbing"; | ||||||
|  |           } | ||||||
|  |         }); | ||||||
|  |  | ||||||
|  |         targetEl.addEventListener("mouseup", (e) => { | ||||||
|  |           if (e.button === 1) { | ||||||
|  |             // Средняя кнопка мыши отпущена | ||||||
|  |             e.preventDefault(); // Предотвращаем скролл страницы | ||||||
|  |             targetEl.style.cursor = "pointer"; | ||||||
|  |           } | ||||||
|  |         }); | ||||||
|  |  | ||||||
|  |         // Дополнительная защита от нежелательного поведения средней кнопки мыши | ||||||
|  |         targetEl.addEventListener("auxclick", (e) => { | ||||||
|  |           if (e.button === 1) { | ||||||
|  |             // Средняя кнопка мыши | ||||||
|  |             e.preventDefault(); // Предотвращаем открытие ссылки в новой вкладке | ||||||
|  |           } | ||||||
|  |         }); | ||||||
|       } |       } | ||||||
|       document.addEventListener("keydown", this.boundHandleKeyDown); |       document.addEventListener("keydown", this.boundHandleKeyDown); | ||||||
|       this.activateEditMode(); |       this.activateEditMode(); | ||||||
| @@ -1260,7 +1392,7 @@ class MapService { | |||||||
|  |  | ||||||
|   public loadFeaturesFromApi( |   public loadFeaturesFromApi( | ||||||
|     _apiStations: typeof mapStore.stations, |     _apiStations: typeof mapStore.stations, | ||||||
|     apiRoutes: typeof mapStore.routes, |     _apiRoutes: typeof mapStore.routes, | ||||||
|     _apiSights: typeof mapStore.sights |     _apiSights: typeof mapStore.sights | ||||||
|   ): void { |   ): void { | ||||||
|     if (!this.map) return; |     if (!this.map) return; | ||||||
| @@ -1272,6 +1404,7 @@ class MapService { | |||||||
|     // Используем фильтрованные данные из mapStore |     // Используем фильтрованные данные из mapStore | ||||||
|     const filteredStations = mapStore.filteredStations; |     const filteredStations = mapStore.filteredStations; | ||||||
|     const filteredSights = mapStore.filteredSights; |     const filteredSights = mapStore.filteredSights; | ||||||
|  |     const filteredRoutes = mapStore.filteredRoutes; | ||||||
|  |  | ||||||
|     filteredStations.forEach((station) => { |     filteredStations.forEach((station) => { | ||||||
|       if (station.longitude == null || station.latitude == null) return; |       if (station.longitude == null || station.latitude == null) return; | ||||||
| @@ -1303,17 +1436,16 @@ class MapService { | |||||||
|       pointFeatures.push(feature); |       pointFeatures.push(feature); | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     apiRoutes.forEach((route) => { |     filteredRoutes.forEach((route) => { | ||||||
|       if (!route.path || route.path.length === 0) return; |       if (!route.path || route.path.length === 0) return; | ||||||
|       const coordinates = route.path |       const coordinates = route.path | ||||||
|         .filter((c) => c && c[0] != null && c[1] != null) |         .filter((c) => c && c[0] != null && c[1] != null) | ||||||
|         .map((c: [number, number]) => |         .map((c: [number, number]) => | ||||||
|           transform([c[1], c[0]], "EPSG:4326", projection) |           transform([c[1], c[0]], "EPSG:4326", projection) | ||||||
|         ); |         ); | ||||||
|  |  | ||||||
|       if (coordinates.length === 0) return; |       if (coordinates.length === 0) return; | ||||||
|  |  | ||||||
|       const routeId = `route-${route.id}`; |       const routeId = `route-${route.id}`; | ||||||
|  |  | ||||||
|       const line = new LineString(coordinates); |       const line = new LineString(coordinates); | ||||||
|       const lineFeature = new Feature({ |       const lineFeature = new Feature({ | ||||||
|         geometry: line, |         geometry: line, | ||||||
| @@ -1322,205 +1454,12 @@ class MapService { | |||||||
|       lineFeature.setId(routeId); |       lineFeature.setId(routeId); | ||||||
|       lineFeature.set("featureType", "route"); |       lineFeature.set("featureType", "route"); | ||||||
|       lineFeatures.push(lineFeature); |       lineFeatures.push(lineFeature); | ||||||
|  |  | ||||||
|       // Не создаем прокси-точки для маршрутов - они должны оставаться только линиями |  | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     this.pointSource.addFeatures(pointFeatures); |     this.pointSource.addFeatures(pointFeatures); | ||||||
|     this.lineSource.addFeatures(lineFeatures); |     this.lineSource.addFeatures(lineFeatures); | ||||||
|  |  | ||||||
|     this.updateFeaturesInReact(); |     this.updateFeaturesInReact(); | ||||||
|     const initialState = this.getCurrentStateAsGeoJSON(); |  | ||||||
|     if (initialState) { |  | ||||||
|       this.addStateToHistory(initialState); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   private addStateToHistory(stateToSave: string): void { |  | ||||||
|     this.history = this.history.slice(0, this.historyIndex + 1); |  | ||||||
|     this.history.push({ state: stateToSave }); |  | ||||||
|     this.historyIndex = this.history.length - 1; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   private getCurrentStateAsGeoJSON(): string | null { |  | ||||||
|     if (!this.map) return null; |  | ||||||
|     const geoJSONFormat = new GeoJSON(); |  | ||||||
|     const allFeatures = [ |  | ||||||
|       ...this.pointSource.getFeatures(), |  | ||||||
|       ...this.lineSource.getFeatures(), |  | ||||||
|     ]; |  | ||||||
|     return geoJSONFormat.writeFeatures(allFeatures, { |  | ||||||
|       dataProjection: "EPSG:4326", |  | ||||||
|       featureProjection: this.map.getView().getProjection().getCode(), |  | ||||||
|     }); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   private applyHistoryState(geoJSONState: string) { |  | ||||||
|     if (!this.map) return; |  | ||||||
|     const projection = this.map.getView().getProjection(); |  | ||||||
|     const geoJSONFormat = new GeoJSON({ |  | ||||||
|       dataProjection: "EPSG:4326", |  | ||||||
|       featureProjection: projection.getCode(), |  | ||||||
|     }); |  | ||||||
|     const features = geoJSONFormat.readFeatures( |  | ||||||
|       geoJSONState |  | ||||||
|     ) as Feature<Geometry>[]; |  | ||||||
|  |  | ||||||
|     this.unselect(); |  | ||||||
|     this.pointSource.clear(); |  | ||||||
|     this.lineSource.clear(); |  | ||||||
|  |  | ||||||
|     const pointFeatures: Feature<Point>[] = []; |  | ||||||
|     const lineFeatures: Feature<LineString>[] = []; |  | ||||||
|  |  | ||||||
|     features.forEach((feature) => { |  | ||||||
|       const featureType = feature.get("featureType"); |  | ||||||
|       const isProxy = feature.get("isProxy"); |  | ||||||
|       if (featureType === "route" && !isProxy) { |  | ||||||
|         lineFeatures.push(feature as Feature<LineString>); |  | ||||||
|       } else { |  | ||||||
|         pointFeatures.push(feature as Feature<Point>); |  | ||||||
|       } |  | ||||||
|     }); |  | ||||||
|  |  | ||||||
|     this.pointSource.addFeatures(pointFeatures); |  | ||||||
|     this.lineSource.addFeatures(lineFeatures); |  | ||||||
|  |  | ||||||
|     this.updateFeaturesInReact(); |  | ||||||
|  |  | ||||||
|     const newStations: ApiStation[] = []; |  | ||||||
|     const newRoutes: ApiRoute[] = []; |  | ||||||
|     const newSights: ApiSight[] = []; |  | ||||||
|  |  | ||||||
|     features.forEach((feature) => { |  | ||||||
|       const id = feature.getId(); |  | ||||||
|       if (!id || feature.get("isProxy")) return; |  | ||||||
|  |  | ||||||
|       const [featureType, numericIdStr] = String(id).split("-"); |  | ||||||
|       const numericId = parseInt(numericIdStr, 10); |  | ||||||
|       if (isNaN(numericId)) return; |  | ||||||
|       const geometry = feature.getGeometry(); |  | ||||||
|       if (!geometry) return; |  | ||||||
|       const properties = feature.getProperties(); |  | ||||||
|  |  | ||||||
|       if (featureType === "station") { |  | ||||||
|         const coords = (geometry as Point).getCoordinates(); |  | ||||||
|         const [lon, lat] = toLonLat(coords, projection); |  | ||||||
|         newStations.push({ |  | ||||||
|           id: numericId, |  | ||||||
|           name: properties.name, |  | ||||||
|           latitude: lat, |  | ||||||
|           longitude: lon, |  | ||||||
|           city_id: properties.city_id || 1, // Default city_id if not available |  | ||||||
|         }); |  | ||||||
|       } else if (featureType === "sight") { |  | ||||||
|         const coords = (geometry as Point).getCoordinates(); |  | ||||||
|         const [lon, lat] = toLonLat(coords, projection); |  | ||||||
|         newSights.push({ |  | ||||||
|           id: numericId, |  | ||||||
|           name: properties.name, |  | ||||||
|           description: properties.description, |  | ||||||
|           latitude: lat, |  | ||||||
|           longitude: lon, |  | ||||||
|           city_id: properties.city_id || 1, // Default city_id if not available |  | ||||||
|         }); |  | ||||||
|       } else if (featureType === "route") { |  | ||||||
|         const coords = (geometry as LineString).getCoordinates(); |  | ||||||
|         const path = coords.map((c) => { |  | ||||||
|           const [lon, lat] = toLonLat(c, projection); |  | ||||||
|           return [lat, lon] as [number, number]; |  | ||||||
|         }); |  | ||||||
|  |  | ||||||
|         const centerCoords = getCenter(geometry.getExtent()); |  | ||||||
|         const [center_longitude, center_latitude] = toLonLat( |  | ||||||
|           centerCoords, |  | ||||||
|           projection |  | ||||||
|         ); |  | ||||||
|  |  | ||||||
|         newRoutes.push({ |  | ||||||
|           id: numericId, |  | ||||||
|           route_number: properties.name, |  | ||||||
|           path: path, |  | ||||||
|           center_latitude, |  | ||||||
|           center_longitude, |  | ||||||
|         }); |  | ||||||
|       } |  | ||||||
|     }); |  | ||||||
|  |  | ||||||
|     mapStore.stations = newStations; |  | ||||||
|     mapStore.routes = newRoutes.sort((a, b) => |  | ||||||
|       a.route_number.localeCompare(b.route_number) |  | ||||||
|     ); |  | ||||||
|     mapStore.sights = newSights; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   public undo(): void { |  | ||||||
|     if (this.historyIndex > 0) { |  | ||||||
|       this.historyIndex--; |  | ||||||
|       const stateToRestore = this.history[this.historyIndex].state; |  | ||||||
|       this.applyHistoryState(stateToRestore); |  | ||||||
|  |  | ||||||
|       const features = [ |  | ||||||
|         ...this.pointSource.getFeatures().filter((f) => !f.get("isProxy")), |  | ||||||
|         ...this.lineSource.getFeatures(), |  | ||||||
|       ]; |  | ||||||
|       const updatePromises = features.map((feature) => { |  | ||||||
|         const featureType = feature.get("featureType"); |  | ||||||
|         const geoJSONFormat = new GeoJSON({ |  | ||||||
|           dataProjection: "EPSG:4326", |  | ||||||
|           featureProjection: this.map?.getView().getProjection().getCode(), |  | ||||||
|         }); |  | ||||||
|         const featureGeoJSON = geoJSONFormat.writeFeatureObject(feature); |  | ||||||
|         return mapStore.updateFeature(featureType, featureGeoJSON); |  | ||||||
|       }); |  | ||||||
|  |  | ||||||
|       Promise.all(updatePromises) |  | ||||||
|         .then(() => {}) |  | ||||||
|         .catch((error) => { |  | ||||||
|           console.error("Failed to update backend after undo:", error); |  | ||||||
|           this.historyIndex++; |  | ||||||
|           const previousState = this.history[this.historyIndex].state; |  | ||||||
|           this.applyHistoryState(previousState); |  | ||||||
|         }); |  | ||||||
|     } else { |  | ||||||
|       toast.info("Больше отменять нечего"); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   public redo(): void { |  | ||||||
|     if (this.historyIndex < this.history.length - 1) { |  | ||||||
|       this.historyIndex++; |  | ||||||
|       const stateToRestore = this.history[this.historyIndex].state; |  | ||||||
|       this.applyHistoryState(stateToRestore); |  | ||||||
|  |  | ||||||
|       const features = [ |  | ||||||
|         ...this.pointSource.getFeatures().filter((f) => !f.get("isProxy")), |  | ||||||
|         ...this.lineSource.getFeatures(), |  | ||||||
|       ]; |  | ||||||
|       const updatePromises = features.map((feature) => { |  | ||||||
|         const featureType = feature.get("featureType"); |  | ||||||
|         const geoJSONFormat = new GeoJSON({ |  | ||||||
|           dataProjection: "EPSG:4326", |  | ||||||
|           featureProjection: this.map?.getView().getProjection().getCode(), |  | ||||||
|         }); |  | ||||||
|         const featureGeoJSON = geoJSONFormat.writeFeatureObject(feature); |  | ||||||
|         return mapStore.updateFeature(featureType, featureGeoJSON); |  | ||||||
|       }); |  | ||||||
|  |  | ||||||
|       Promise.all(updatePromises) |  | ||||||
|         .then(() => { |  | ||||||
|           toast.info("Действие повторено"); |  | ||||||
|         }) |  | ||||||
|         .catch((error) => { |  | ||||||
|           console.error("Failed to update backend after redo:", error); |  | ||||||
|           toast.error("Не удалось обновить данные на сервере"); |  | ||||||
|           this.historyIndex--; |  | ||||||
|           const previousState = this.history[this.historyIndex].state; |  | ||||||
|           this.applyHistoryState(previousState); |  | ||||||
|         }); |  | ||||||
|     } else { |  | ||||||
|       toast.info("Больше повторять нечего"); |  | ||||||
|     } |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   private updateFeaturesInReact(): void { |   private updateFeaturesInReact(): void { | ||||||
| @@ -1540,19 +1479,17 @@ class MapService { | |||||||
|       this.routeLayer.changed(); |       this.routeLayer.changed(); | ||||||
|     } |     } | ||||||
|     if (this.tooltipOverlay) this.tooltipOverlay.setPosition(undefined); |     if (this.tooltipOverlay) this.tooltipOverlay.setPosition(undefined); | ||||||
|  |  | ||||||
|  |     // Сбрасываем курсор при покидании области карты | ||||||
|  |     if (this.map) { | ||||||
|  |       const targetEl = this.map.getTargetElement(); | ||||||
|  |       if (targetEl instanceof HTMLElement) { | ||||||
|  |         targetEl.style.cursor = "pointer"; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   private handleKeyDown(event: KeyboardEvent): void { |   private handleKeyDown(event: KeyboardEvent): void { | ||||||
|     if ((event.ctrlKey || event.metaKey) && event.key === "z") { |  | ||||||
|       event.preventDefault(); |  | ||||||
|       this.undo(); |  | ||||||
|       return; |  | ||||||
|     } |  | ||||||
|     if ((event.ctrlKey || event.metaKey) && event.key === "y") { |  | ||||||
|       event.preventDefault(); |  | ||||||
|       this.redo(); |  | ||||||
|       return; |  | ||||||
|     } |  | ||||||
|     if (event.key === "Escape") { |     if (event.key === "Escape") { | ||||||
|       this.unselect(); |       this.unselect(); | ||||||
|     } |     } | ||||||
| @@ -1619,36 +1556,35 @@ class MapService { | |||||||
|       style: styleForDrawing, |       style: styleForDrawing, | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     this.currentInteraction.on("drawstart", () => { |  | ||||||
|       this.beforeActionState = this.getCurrentStateAsGeoJSON(); |  | ||||||
|     }); |  | ||||||
|  |  | ||||||
|     this.currentInteraction.on("drawend", async (event: DrawEvent) => { |     this.currentInteraction.on("drawend", async (event: DrawEvent) => { | ||||||
|       if (this.beforeActionState) { |  | ||||||
|         this.addStateToHistory(this.beforeActionState); |  | ||||||
|       } |  | ||||||
|       this.beforeActionState = null; |  | ||||||
|  |  | ||||||
|       const feature = event.feature as Feature<Geometry>; |       const feature = event.feature as Feature<Geometry>; | ||||||
|       const fType = this.currentDrawingFeatureType; |       const fType = this.currentDrawingFeatureType; | ||||||
|       if (!fType) return; |       if (!fType) return; | ||||||
|  |  | ||||||
|  |       // Проверяем, не идет ли уже процесс создания | ||||||
|  |       if (this.isCreating) { | ||||||
|  |         toast.warning("Дождитесь завершения создания предыдущего объекта."); | ||||||
|  |         // Удаляем созданный объект из источника | ||||||
|  |         const sourceForDrawing = | ||||||
|  |           type === "Point" ? this.pointSource : this.lineSource; | ||||||
|  |         setTimeout(() => { | ||||||
|  |           if (sourceForDrawing.hasFeature(feature as any)) { | ||||||
|  |             sourceForDrawing.removeFeature(feature as any); | ||||||
|  |           } | ||||||
|  |         }, 0); | ||||||
|  |         return; | ||||||
|  |       } | ||||||
|  |  | ||||||
|       feature.set("featureType", fType); |       feature.set("featureType", fType); | ||||||
|  |  | ||||||
|       let resourceName: string; |       let resourceName: string; | ||||||
|       const allFeatures = [ |  | ||||||
|         ...this.pointSource.getFeatures(), |  | ||||||
|         ...this.lineSource.getFeatures(), |  | ||||||
|       ]; |  | ||||||
|  |  | ||||||
|       switch (fType) { |       switch (fType) { | ||||||
|         case "station": |         case "station": | ||||||
|           const existingStations = allFeatures.filter( |           // Используем полный список из mapStore, а не отфильтрованный | ||||||
|             (f) => f.get("featureType") === "station" |           const stationNumbers = mapStore.stations | ||||||
|           ); |             .map((station) => { | ||||||
|           const stationNumbers = existingStations |               const match = station.name?.match(/^Остановка (\d+)$/); | ||||||
|             .map((f) => { |  | ||||||
|               const name = f.get("name") as string; |  | ||||||
|               const match = name?.match(/^Остановка (\d+)$/); |  | ||||||
|               return match ? parseInt(match[1], 10) : 0; |               return match ? parseInt(match[1], 10) : 0; | ||||||
|             }) |             }) | ||||||
|             .filter((num) => num > 0); |             .filter((num) => num > 0); | ||||||
| @@ -1657,13 +1593,10 @@ class MapService { | |||||||
|           resourceName = `Остановка ${nextStationNumber}`; |           resourceName = `Остановка ${nextStationNumber}`; | ||||||
|           break; |           break; | ||||||
|         case "sight": |         case "sight": | ||||||
|           const existingSights = allFeatures.filter( |           // Используем полный список из mapStore, а не отфильтрованный | ||||||
|             (f) => f.get("featureType") === "sight" |           const sightNumbers = mapStore.sights | ||||||
|           ); |             .map((sight) => { | ||||||
|           const sightNumbers = existingSights |               const match = sight.name?.match(/^Достопримечательность (\d+)$/); | ||||||
|             .map((f) => { |  | ||||||
|               const name = f.get("name") as string; |  | ||||||
|               const match = name?.match(/^Достопримечательность (\d+)$/); |  | ||||||
|               return match ? parseInt(match[1], 10) : 0; |               return match ? parseInt(match[1], 10) : 0; | ||||||
|             }) |             }) | ||||||
|             .filter((num) => num > 0); |             .filter((num) => num > 0); | ||||||
| @@ -1672,13 +1605,10 @@ class MapService { | |||||||
|           resourceName = `Достопримечательность ${nextSightNumber}`; |           resourceName = `Достопримечательность ${nextSightNumber}`; | ||||||
|           break; |           break; | ||||||
|         case "route": |         case "route": | ||||||
|           const existingRoutes = allFeatures.filter( |           // Используем полный список из mapStore, а не отфильтрованный | ||||||
|             (f) => f.get("featureType") === "route" && !f.get("isProxy") |           const routeNumbers = mapStore.routes | ||||||
|           ); |             .map((route) => { | ||||||
|           const routeNumbers = existingRoutes |               const match = route.route_number?.match(/^Маршрут (\d+)$/); | ||||||
|             .map((f) => { |  | ||||||
|               const name = f.get("name") as string; |  | ||||||
|               const match = name?.match(/^Маршрут (\d+)$/); |  | ||||||
|               return match ? parseInt(match[1], 10) : 0; |               return match ? parseInt(match[1], 10) : 0; | ||||||
|             }) |             }) | ||||||
|             .filter((num) => num > 0); |             .filter((num) => num > 0); | ||||||
| @@ -1731,6 +1661,13 @@ class MapService { | |||||||
|  |  | ||||||
|   public finishDrawing(): void { |   public finishDrawing(): void { | ||||||
|     if (!this.currentInteraction) return; |     if (!this.currentInteraction) return; | ||||||
|  |  | ||||||
|  |     // Блокируем завершение рисования, если идет процесс создания | ||||||
|  |     if (this.isCreating) { | ||||||
|  |       toast.warning("Дождитесь завершения создания предыдущего объекта."); | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|     try { |     try { | ||||||
|       this.currentInteraction.finishDrawing(); |       this.currentInteraction.finishDrawing(); | ||||||
|     } catch (e) { |     } catch (e) { | ||||||
| @@ -1756,7 +1693,8 @@ class MapService { | |||||||
|       layerFilter, |       layerFilter, | ||||||
|       hitTolerance: 5, |       hitTolerance: 5, | ||||||
|     }); |     }); | ||||||
|     this.map.getTargetElement().style.cursor = hit ? "pointer" : ""; |     // Устанавливаем курсор pointer для всей карты, чтобы показать возможность перетаскивания колёсиком | ||||||
|  |     this.map.getTargetElement().style.cursor = hit ? "pointer" : "pointer"; | ||||||
|  |  | ||||||
|     const featureAtPixel: Feature<Geometry> | undefined = |     const featureAtPixel: Feature<Geometry> | undefined = | ||||||
|       this.map.forEachFeatureAtPixel( |       this.map.forEachFeatureAtPixel( | ||||||
| @@ -1841,18 +1779,12 @@ class MapService { | |||||||
|   ): void { |   ): void { | ||||||
|     if (featureId === undefined) return; |     if (featureId === undefined) return; | ||||||
|  |  | ||||||
|     this.beforeActionState = this.getCurrentStateAsGeoJSON(); |  | ||||||
|  |  | ||||||
|     const numericId = parseInt(String(featureId).split("-")[1], 10); |     const numericId = parseInt(String(featureId).split("-")[1], 10); | ||||||
|     if (!recourse || isNaN(numericId)) return; |     if (!recourse || isNaN(numericId)) return; | ||||||
|  |  | ||||||
|     mapStore |     mapStore | ||||||
|       .deleteFeature(recourse, numericId) |       .deleteFeature(recourse, numericId) | ||||||
|       .then(() => { |       .then(() => { | ||||||
|         if (this.beforeActionState) |  | ||||||
|           this.addStateToHistory(this.beforeActionState); |  | ||||||
|         this.beforeActionState = null; |  | ||||||
|  |  | ||||||
|         if (recourse === "route") { |         if (recourse === "route") { | ||||||
|           const lineFeature = this.lineSource.getFeatureById(featureId); |           const lineFeature = this.lineSource.getFeatureById(featureId); | ||||||
|           if (lineFeature) |           if (lineFeature) | ||||||
| @@ -1877,8 +1809,6 @@ class MapService { | |||||||
|   public deleteMultipleFeatures(featureIds: (string | number)[]): void { |   public deleteMultipleFeatures(featureIds: (string | number)[]): void { | ||||||
|     if (!featureIds || featureIds.length === 0) return; |     if (!featureIds || featureIds.length === 0) return; | ||||||
|  |  | ||||||
|     this.beforeActionState = this.getCurrentStateAsGeoJSON(); |  | ||||||
|  |  | ||||||
|     const deletePromises = Array.from(featureIds).map((id) => { |     const deletePromises = Array.from(featureIds).map((id) => { | ||||||
|       const recourse = String(id).split("-")[0]; |       const recourse = String(id).split("-")[0]; | ||||||
|       const numericId = parseInt(String(id).split("-")[1], 10); |       const numericId = parseInt(String(id).split("-")[1], 10); | ||||||
| @@ -1895,10 +1825,6 @@ class MapService { | |||||||
|           | number |           | number | ||||||
|         )[]; |         )[]; | ||||||
|         if (successfulDeletes.length > 0) { |         if (successfulDeletes.length > 0) { | ||||||
|           if (this.beforeActionState) |  | ||||||
|             this.addStateToHistory(this.beforeActionState); |  | ||||||
|           this.beforeActionState = null; |  | ||||||
|  |  | ||||||
|           successfulDeletes.forEach((id) => { |           successfulDeletes.forEach((id) => { | ||||||
|             const recourse = String(id).split("-")[0]; |             const recourse = String(id).split("-")[0]; | ||||||
|             if (recourse === "route") { |             if (recourse === "route") { | ||||||
| @@ -2029,9 +1955,6 @@ class MapService { | |||||||
|   // Метод для сброса кешей карты |   // Метод для сброса кешей карты | ||||||
|   public clearCaches() { |   public clearCaches() { | ||||||
|     this.clusterStyleCache = {}; |     this.clusterStyleCache = {}; | ||||||
|     this.history = []; |  | ||||||
|     this.historyIndex = -1; |  | ||||||
|     this.beforeActionState = null; |  | ||||||
|     this.hoveredFeatureId = null; |     this.hoveredFeatureId = null; | ||||||
|     this.selectedIds.clear(); |     this.selectedIds.clear(); | ||||||
|  |  | ||||||
| @@ -2086,9 +2009,6 @@ class MapService { | |||||||
|     } catch (error) { |     } catch (error) { | ||||||
|       console.error("Failed to update feature:", error); |       console.error("Failed to update feature:", error); | ||||||
|       toast.error(`Не удалось обновить: ${error}`); |       toast.error(`Не удалось обновить: ${error}`); | ||||||
|       if (this.beforeActionState) { |  | ||||||
|         this.applyHistoryState(this.beforeActionState); |  | ||||||
|       } |  | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -2096,6 +2016,22 @@ class MapService { | |||||||
|     const featureType = feature.get("featureType") as FeatureType; |     const featureType = feature.get("featureType") as FeatureType; | ||||||
|     if (!featureType || !this.map) return; |     if (!featureType || !this.map) return; | ||||||
|  |  | ||||||
|  |     // Проверяем, не идет ли уже процесс создания | ||||||
|  |     if (this.isCreating) { | ||||||
|  |       toast.warning("Дождитесь завершения создания предыдущего объекта."); | ||||||
|  |       // Удаляем незавершенный объект с карты | ||||||
|  |       if (feature.getGeometry()?.getType() === "LineString") { | ||||||
|  |         if (this.lineSource.hasFeature(feature as Feature<LineString>)) | ||||||
|  |           this.lineSource.removeFeature(feature as Feature<LineString>); | ||||||
|  |       } else { | ||||||
|  |         if (this.pointSource.hasFeature(feature as Feature<Point>)) | ||||||
|  |           this.pointSource.removeFeature(feature as Feature<Point>); | ||||||
|  |       } | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     this.isCreating = true; | ||||||
|  |  | ||||||
|     const geoJSONFormat = new GeoJSON({ |     const geoJSONFormat = new GeoJSON({ | ||||||
|       dataProjection: "EPSG:4326", |       dataProjection: "EPSG:4326", | ||||||
|       featureProjection: this.map.getView().getProjection().getCode(), |       featureProjection: this.map.getView().getProjection().getCode(), | ||||||
| @@ -2156,10 +2092,8 @@ class MapService { | |||||||
|         if (this.pointSource.hasFeature(feature as Feature<Point>)) |         if (this.pointSource.hasFeature(feature as Feature<Point>)) | ||||||
|           this.pointSource.removeFeature(feature as Feature<Point>); |           this.pointSource.removeFeature(feature as Feature<Point>); | ||||||
|       } |       } | ||||||
|       if (this.beforeActionState) { |     } finally { | ||||||
|         this.applyHistoryState(this.beforeActionState); |       this.isCreating = false; | ||||||
|       } |  | ||||||
|       this.beforeActionState = null; |  | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| } | } | ||||||
| @@ -2332,14 +2266,21 @@ const MapSightbar: React.FC<MapSightbarProps> = observer( | |||||||
|         return feature; |         return feature; | ||||||
|       }); |       }); | ||||||
|  |  | ||||||
|       const lines = actualFeatures.filter( |       const lines = mapStore.filteredRoutes.map((route) => { | ||||||
|         (f) => f.get("featureType") === "route" |         const feature = new Feature({ | ||||||
|       ); |           geometry: new LineString(route.path), | ||||||
|  |           name: route.route_number, | ||||||
|  |         }); | ||||||
|  |         feature.setId(`route-${route.id}`); | ||||||
|  |         feature.set("featureType", "route"); | ||||||
|  |         return feature; | ||||||
|  |       }); | ||||||
|  |  | ||||||
|       return [...stations, ...sights, ...lines]; |       return [...stations, ...sights, ...lines]; | ||||||
|     }, [ |     }, [ | ||||||
|       mapStore.filteredStations, |       mapStore.filteredStations, | ||||||
|       mapStore.filteredSights, |       mapStore.filteredSights, | ||||||
|  |       mapStore.filteredRoutes, | ||||||
|       actualFeatures, |       actualFeatures, | ||||||
|       selectedCityId, |       selectedCityId, | ||||||
|       mapStore, |       mapStore, | ||||||
| @@ -2423,7 +2364,7 @@ const MapSightbar: React.FC<MapSightbarProps> = observer( | |||||||
|               (a.get("name") as string) || "" |               (a.get("name") as string) || "" | ||||||
|             ) |             ) | ||||||
|           ); |           ); | ||||||
|         case "date_asc": |         case "created_asc": | ||||||
|           return sorted.sort((a, b) => { |           return sorted.sort((a, b) => { | ||||||
|             const aDate = a.get("created_at") |             const aDate = a.get("created_at") | ||||||
|               ? new Date(a.get("created_at")) |               ? new Date(a.get("created_at")) | ||||||
| @@ -2433,7 +2374,7 @@ const MapSightbar: React.FC<MapSightbarProps> = observer( | |||||||
|               : new Date(0); |               : new Date(0); | ||||||
|             return aDate.getTime() - bDate.getTime(); |             return aDate.getTime() - bDate.getTime(); | ||||||
|           }); |           }); | ||||||
|         case "date_desc": |         case "created_desc": | ||||||
|           return sorted.sort((a, b) => { |           return sorted.sort((a, b) => { | ||||||
|             const aDate = a.get("created_at") |             const aDate = a.get("created_at") | ||||||
|               ? new Date(a.get("created_at")) |               ? new Date(a.get("created_at")) | ||||||
| @@ -2443,6 +2384,34 @@ const MapSightbar: React.FC<MapSightbarProps> = observer( | |||||||
|               : new Date(0); |               : new Date(0); | ||||||
|             return bDate.getTime() - aDate.getTime(); |             return bDate.getTime() - aDate.getTime(); | ||||||
|           }); |           }); | ||||||
|  |         case "updated_asc": | ||||||
|  |           return sorted.sort((a, b) => { | ||||||
|  |             const aDate = a.get("updated_at") | ||||||
|  |               ? new Date(a.get("updated_at")) | ||||||
|  |               : a.get("created_at") | ||||||
|  |               ? new Date(a.get("created_at")) | ||||||
|  |               : new Date(0); | ||||||
|  |             const bDate = b.get("updated_at") | ||||||
|  |               ? new Date(b.get("updated_at")) | ||||||
|  |               : b.get("created_at") | ||||||
|  |               ? new Date(b.get("created_at")) | ||||||
|  |               : new Date(0); | ||||||
|  |             return aDate.getTime() - bDate.getTime(); | ||||||
|  |           }); | ||||||
|  |         case "updated_desc": | ||||||
|  |           return sorted.sort((a, b) => { | ||||||
|  |             const aDate = a.get("updated_at") | ||||||
|  |               ? new Date(a.get("updated_at")) | ||||||
|  |               : a.get("created_at") | ||||||
|  |               ? new Date(a.get("created_at")) | ||||||
|  |               : new Date(0); | ||||||
|  |             const bDate = b.get("updated_at") | ||||||
|  |               ? new Date(b.get("updated_at")) | ||||||
|  |               : b.get("created_at") | ||||||
|  |               ? new Date(b.get("created_at")) | ||||||
|  |               : new Date(0); | ||||||
|  |             return bDate.getTime() - aDate.getTime(); | ||||||
|  |           }); | ||||||
|         default: |         default: | ||||||
|           return sorted; |           return sorted; | ||||||
|       } |       } | ||||||
| @@ -2575,6 +2544,10 @@ const MapSightbar: React.FC<MapSightbarProps> = observer( | |||||||
|             > |             > | ||||||
|               <option value="name_asc">Имя ↑</option> |               <option value="name_asc">Имя ↑</option> | ||||||
|               <option value="name_desc">Имя ↓</option> |               <option value="name_desc">Имя ↓</option> | ||||||
|  |               <option value="created_asc">Дата создания ↑</option> | ||||||
|  |               <option value="created_desc">Дата создания ↓</option> | ||||||
|  |               <option value="updated_asc">Дата обновления ↑</option> | ||||||
|  |               <option value="updated_desc">Дата обновления ↓</option> | ||||||
|             </select> |             </select> | ||||||
|           </div> |           </div> | ||||||
|         ), |         ), | ||||||
| @@ -2603,6 +2576,10 @@ const MapSightbar: React.FC<MapSightbarProps> = observer( | |||||||
|             > |             > | ||||||
|               <option value="name_asc">Имя ↑</option> |               <option value="name_asc">Имя ↑</option> | ||||||
|               <option value="name_desc">Имя ↓</option> |               <option value="name_desc">Имя ↓</option> | ||||||
|  |               <option value="created_asc">Дата создания ↑</option> | ||||||
|  |               <option value="created_desc">Дата создания ↓</option> | ||||||
|  |               <option value="updated_asc">Дата обновления ↑</option> | ||||||
|  |               <option value="updated_desc">Дата обновления ↓</option> | ||||||
|             </select> |             </select> | ||||||
|           </div> |           </div> | ||||||
|         ), |         ), | ||||||
| @@ -2610,10 +2587,6 @@ const MapSightbar: React.FC<MapSightbarProps> = observer( | |||||||
|       }, |       }, | ||||||
|     ]; |     ]; | ||||||
|  |  | ||||||
|     React.useEffect(() => { |  | ||||||
|       console.log("isOpen changed:", isOpen); |  | ||||||
|     }, [isOpen]); |  | ||||||
|  |  | ||||||
|     return ( |     return ( | ||||||
|       <div |       <div | ||||||
|         className={`${ |         className={`${ | ||||||
| @@ -2772,6 +2745,7 @@ export const MapPage: React.FC = observer(() => { | |||||||
|             mapStore.getRoutes(), |             mapStore.getRoutes(), | ||||||
|             mapStore.getStations(), |             mapStore.getStations(), | ||||||
|             mapStore.getSights(), |             mapStore.getSights(), | ||||||
|  |             carrierStore.getCarriers("ru"), | ||||||
|           ]); |           ]); | ||||||
|           mapService.loadFeaturesFromApi( |           mapService.loadFeaturesFromApi( | ||||||
|             mapStore.stations, |             mapStore.stations, | ||||||
| @@ -2994,18 +2968,6 @@ export const MapPage: React.FC = observer(() => { | |||||||
|                 <span className="font-mono bg-gray-100 px-1 rounded">Esc</span>{" "} |                 <span className="font-mono bg-gray-100 px-1 rounded">Esc</span>{" "} | ||||||
|                 - Отменить выделение |                 - Отменить выделение | ||||||
|               </li> |               </li> | ||||||
|               <li> |  | ||||||
|                 <span className="font-mono bg-gray-100 px-1 rounded"> |  | ||||||
|                   Ctrl+Z |  | ||||||
|                 </span>{" "} |  | ||||||
|                 - Отменить действие |  | ||||||
|               </li> |  | ||||||
|               <li> |  | ||||||
|                 <span className="font-mono bg-gray-100 px-1 rounded"> |  | ||||||
|                   Ctrl+Y |  | ||||||
|                 </span>{" "} |  | ||||||
|                 - Повторить действие |  | ||||||
|               </li> |  | ||||||
|             </ul> |             </ul> | ||||||
|             <button |             <button | ||||||
|               onClick={() => setShowHelp(false)} |               onClick={() => setShowHelp(false)} | ||||||
|   | |||||||
| @@ -60,7 +60,11 @@ export const MediaEditPage = observer(() => { | |||||||
|       if (extension) { |       if (extension) { | ||||||
|         if (["glb", "gltf"].includes(extension)) { |         if (["glb", "gltf"].includes(extension)) { | ||||||
|           setAvailableMediaTypes([6]); // 3D model |           setAvailableMediaTypes([6]); // 3D model | ||||||
|         } else if (["jpg", "jpeg", "png", "gif"].includes(extension)) { |         } else if ( | ||||||
|  |           ["jpg", "jpeg", "png", "gif", "svg", "webp", "bmp"].includes( | ||||||
|  |             extension | ||||||
|  |           ) | ||||||
|  |         ) { | ||||||
|           setAvailableMediaTypes([1, 3, 4, 5]); // Photo, Icon, Watermark, Panorama |           setAvailableMediaTypes([1, 3, 4, 5]); // Photo, Icon, Watermark, Panorama | ||||||
|         } else if (["mp4", "webm", "mov"].includes(extension)) { |         } else if (["mp4", "webm", "mov"].includes(extension)) { | ||||||
|           setAvailableMediaTypes([2]); // Video |           setAvailableMediaTypes([2]); // Video | ||||||
| @@ -109,7 +113,11 @@ export const MediaEditPage = observer(() => { | |||||||
|         if (["glb", "gltf"].includes(extension)) { |         if (["glb", "gltf"].includes(extension)) { | ||||||
|           setAvailableMediaTypes([6]); // 3D model |           setAvailableMediaTypes([6]); // 3D model | ||||||
|           setMediaType(6); |           setMediaType(6); | ||||||
|         } else if (["jpg", "jpeg", "png", "gif"].includes(extension)) { |         } else if ( | ||||||
|  |           ["jpg", "jpeg", "png", "gif", "svg", "webp", "bmp"].includes( | ||||||
|  |             extension | ||||||
|  |           ) | ||||||
|  |         ) { | ||||||
|           setAvailableMediaTypes([1, 3, 4, 5]); // Photo, Icon, Watermark, Panorama |           setAvailableMediaTypes([1, 3, 4, 5]); // Photo, Icon, Watermark, Panorama | ||||||
|           setMediaType(1); // Default to Photo |           setMediaType(1); // Default to Photo | ||||||
|         } else if (["mp4", "webm", "mov"].includes(extension)) { |         } else if (["mp4", "webm", "mov"].includes(extension)) { | ||||||
|   | |||||||
| @@ -69,6 +69,7 @@ export const MediaListPage = observer(() => { | |||||||
|       width: 200, |       width: 200, | ||||||
|       align: "center", |       align: "center", | ||||||
|       headerAlign: "center", |       headerAlign: "center", | ||||||
|  |       sortable: false, | ||||||
|  |  | ||||||
|       renderCell: (params: GridRenderCellParams) => { |       renderCell: (params: GridRenderCellParams) => { | ||||||
|         return ( |         return ( | ||||||
|   | |||||||
| @@ -24,6 +24,7 @@ import { | |||||||
|   Tab, |   Tab, | ||||||
|   Box, |   Box, | ||||||
| } from "@mui/material"; | } from "@mui/material"; | ||||||
|  | import { observer } from "mobx-react-lite"; | ||||||
| import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; | import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; | ||||||
| import DragIndicatorIcon from "@mui/icons-material/DragIndicator"; | import DragIndicatorIcon from "@mui/icons-material/DragIndicator"; | ||||||
| import { | import { | ||||||
| @@ -33,7 +34,12 @@ import { | |||||||
|   DropResult, |   DropResult, | ||||||
| } from "@hello-pangea/dnd"; | } from "@hello-pangea/dnd"; | ||||||
|  |  | ||||||
| import { authInstance, languageStore, routeStore } from "@shared"; | import { | ||||||
|  |   authInstance, | ||||||
|  |   languageStore, | ||||||
|  |   routeStore, | ||||||
|  |   selectedCityStore, | ||||||
|  | } from "@shared"; | ||||||
| import { EditStationModal } from "../../widgets/modals/EditStationModal"; | import { EditStationModal } from "../../widgets/modals/EditStationModal"; | ||||||
|  |  | ||||||
| // Helper function to insert an item at a specific position (1-based index) | // Helper function to insert an item at a specific position (1-based index) | ||||||
| @@ -73,7 +79,6 @@ type LinkedItemsProps<T> = { | |||||||
|   disableCreation?: boolean; |   disableCreation?: boolean; | ||||||
|   updatedLinkedItems?: T[]; |   updatedLinkedItems?: T[]; | ||||||
|   refresh?: number; |   refresh?: number; | ||||||
|   cityId?: number; |  | ||||||
|   routeDirection?: boolean; |   routeDirection?: boolean; | ||||||
| }; | }; | ||||||
|  |  | ||||||
| @@ -112,7 +117,7 @@ export const LinkedItems = < | |||||||
|   ); |   ); | ||||||
| }; | }; | ||||||
|  |  | ||||||
| export const LinkedItemsContents = < | const LinkedItemsContentsInner = < | ||||||
|   T extends { id: number; name: string; [key: string]: any } |   T extends { id: number; name: string; [key: string]: any } | ||||||
| >({ | >({ | ||||||
|   parentId, |   parentId, | ||||||
| @@ -124,7 +129,6 @@ export const LinkedItemsContents = < | |||||||
|   disableCreation = false, |   disableCreation = false, | ||||||
|   updatedLinkedItems, |   updatedLinkedItems, | ||||||
|   refresh, |   refresh, | ||||||
|   cityId, |  | ||||||
|   routeDirection, |   routeDirection, | ||||||
| }: LinkedItemsProps<T>) => { | }: LinkedItemsProps<T>) => { | ||||||
|   const { language } = languageStore; |   const { language } = languageStore; | ||||||
| @@ -153,17 +157,20 @@ export const LinkedItemsContents = < | |||||||
|       // Фильтруем станции по направлению маршрута |       // Фильтруем станции по направлению маршрута | ||||||
|       return item.direction === routeDirection; |       return item.direction === routeDirection; | ||||||
|     }) |     }) | ||||||
|  |     .filter((item) => { | ||||||
|  |       // Фильтруем по городу из навбара | ||||||
|  |       const selectedCityId = selectedCityStore.selectedCityId; | ||||||
|  |       if (selectedCityId && "city_id" in item) { | ||||||
|  |         return item.city_id === selectedCityId; | ||||||
|  |       } | ||||||
|  |       return true; | ||||||
|  |     }) | ||||||
|     .sort((a, b) => a.name.localeCompare(b.name)); |     .sort((a, b) => a.name.localeCompare(b.name)); | ||||||
|  |  | ||||||
|   // Фильтрация по поиску для массового режима |   // Фильтрация по поиску для массового режима | ||||||
|   const filteredAvailableItems = availableItems.filter((item) => { |   const filteredAvailableItems = availableItems.filter((item) => { | ||||||
|     if (!cityId || item.city_id == cityId) { |     if (!searchQuery.trim()) return true; | ||||||
|       if (!searchQuery.trim()) return true; |     return String(item.name).toLowerCase().includes(searchQuery.toLowerCase()); | ||||||
|       return String(item.name) |  | ||||||
|         .toLowerCase() |  | ||||||
|         .includes(searchQuery.toLowerCase()); |  | ||||||
|     } |  | ||||||
|     return false; |  | ||||||
|   }); |   }); | ||||||
|  |  | ||||||
|   useEffect(() => { |   useEffect(() => { | ||||||
| @@ -460,9 +467,7 @@ export const LinkedItemsContents = < | |||||||
|                   onChange={(_, newValue) => |                   onChange={(_, newValue) => | ||||||
|                     setSelectedItemId(newValue?.id || null) |                     setSelectedItemId(newValue?.id || null) | ||||||
|                   } |                   } | ||||||
|                   options={availableItems.filter( |                   options={availableItems} | ||||||
|                     (item) => !cityId || item.city_id == cityId |  | ||||||
|                   )} |  | ||||||
|                   getOptionLabel={(item) => String(item.name)} |                   getOptionLabel={(item) => String(item.name)} | ||||||
|                   renderInput={(params) => ( |                   renderInput={(params) => ( | ||||||
|                     <TextField |                     <TextField | ||||||
| @@ -597,3 +602,7 @@ export const LinkedItemsContents = < | |||||||
|     </> |     </> | ||||||
|   ); |   ); | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | export const LinkedItemsContents = observer( | ||||||
|  |   LinkedItemsContentsInner | ||||||
|  | ) as typeof LinkedItemsContentsInner; | ||||||
|   | |||||||
| @@ -16,13 +16,18 @@ import { | |||||||
| import { MediaViewer } from "@widgets"; | import { MediaViewer } from "@widgets"; | ||||||
| import { observer } from "mobx-react-lite"; | import { observer } from "mobx-react-lite"; | ||||||
| import { ArrowLeft, Loader2, Save, Plus } from "lucide-react"; | import { ArrowLeft, Loader2, Save, Plus } from "lucide-react"; | ||||||
| import { useEffect, useState } from "react"; | import { useEffect, useState, useMemo } from "react"; | ||||||
| import { useNavigate } from "react-router-dom"; | import { useNavigate } from "react-router-dom"; | ||||||
| import { toast } from "react-toastify"; | import { toast } from "react-toastify"; | ||||||
| import { carrierStore } from "../../../shared/store/CarrierStore"; | import { carrierStore } from "../../../shared/store/CarrierStore"; | ||||||
| import { articlesStore } from "../../../shared/store/ArticlesStore"; | import { articlesStore } from "../../../shared/store/ArticlesStore"; | ||||||
| import { Route, routeStore } from "../../../shared/store/RouteStore"; | import { Route, routeStore } from "../../../shared/store/RouteStore"; | ||||||
| import { languageStore, SelectArticleModal, SelectMediaDialog } from "@shared"; | import { | ||||||
|  |   languageStore, | ||||||
|  |   SelectArticleModal, | ||||||
|  |   SelectMediaDialog, | ||||||
|  |   selectedCityStore, | ||||||
|  | } from "@shared"; | ||||||
|  |  | ||||||
| export const RouteCreatePage = observer(() => { | export const RouteCreatePage = observer(() => { | ||||||
|   const navigate = useNavigate(); |   const navigate = useNavigate(); | ||||||
| @@ -50,6 +55,21 @@ export const RouteCreatePage = observer(() => { | |||||||
|     articlesStore.getArticleList(); |     articlesStore.getArticleList(); | ||||||
|   }, [language]); |   }, [language]); | ||||||
|  |  | ||||||
|  |   // Фильтруем перевозчиков только из выбранного города | ||||||
|  |   const filteredCarriers = useMemo(() => { | ||||||
|  |     const carriers = | ||||||
|  |       carrierStore.carriers[language as keyof typeof carrierStore.carriers] | ||||||
|  |         .data || []; | ||||||
|  |  | ||||||
|  |     if (!selectedCityStore.selectedCityId) { | ||||||
|  |       return carriers; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return carriers.filter( | ||||||
|  |       (carrier: any) => carrier.city_id === selectedCityStore.selectedCityId | ||||||
|  |     ); | ||||||
|  |   }, [carrierStore.carriers, language, selectedCityStore.selectedCityId]); | ||||||
|  |  | ||||||
|   const validateCoordinates = (value: string) => { |   const validateCoordinates = (value: string) => { | ||||||
|     try { |     try { | ||||||
|       const lines = value.trim().split("\n"); |       const lines = value.trim().split("\n"); | ||||||
| @@ -194,16 +214,10 @@ export const RouteCreatePage = observer(() => { | |||||||
|               value={carrier} |               value={carrier} | ||||||
|               label="Выберите перевозчика" |               label="Выберите перевозчика" | ||||||
|               onChange={(e) => setCarrier(e.target.value as string)} |               onChange={(e) => setCarrier(e.target.value as string)} | ||||||
|               disabled={ |               disabled={filteredCarriers.length === 0} | ||||||
|                 carrierStore.carriers[ |  | ||||||
|                   language as keyof typeof carrierStore.carriers |  | ||||||
|                 ].data?.length === 0 |  | ||||||
|               } |  | ||||||
|             > |             > | ||||||
|               <MenuItem value="">Не выбрано</MenuItem> |               <MenuItem value="">Не выбрано</MenuItem> | ||||||
|               {carrierStore.carriers[ |               {filteredCarriers.map((carrier: any) => ( | ||||||
|                 language as keyof typeof carrierStore.carriers |  | ||||||
|               ].data?.map((carrier) => ( |  | ||||||
|                 <MenuItem key={carrier.id} value={carrier.id}> |                 <MenuItem key={carrier.id} value={carrier.id}> | ||||||
|                   {carrier.full_name} |                   {carrier.full_name} | ||||||
|                 </MenuItem> |                 </MenuItem> | ||||||
|   | |||||||
| @@ -90,6 +90,7 @@ export const RouteListPage = observer(() => { | |||||||
|       width: 250, |       width: 250, | ||||||
|       align: "center", |       align: "center", | ||||||
|       headerAlign: "center", |       headerAlign: "center", | ||||||
|  |       sortable: false, | ||||||
|       renderCell: (params: GridRenderCellParams) => { |       renderCell: (params: GridRenderCellParams) => { | ||||||
|         return ( |         return ( | ||||||
|           <div className="flex h-full gap-7 justify-center items-center"> |           <div className="flex h-full gap-7 justify-center items-center"> | ||||||
|   | |||||||
| @@ -54,16 +54,16 @@ export function InfiniteCanvas({ | |||||||
|   const lastOriginalRotation = useRef<number | undefined>(undefined); |   const lastOriginalRotation = useRef<number | undefined>(undefined); | ||||||
|  |  | ||||||
|   useEffect(() => { |   useEffect(() => { | ||||||
|     const canvas = applicationRef?.app?.canvas; |     if (!applicationRef?.app?.canvas) return; | ||||||
|     if (!canvas) return; |  | ||||||
|  |  | ||||||
|  |     const canvas = applicationRef.app.canvas; | ||||||
|     const canvasRect = canvas.getBoundingClientRect(); |     const canvasRect = canvas.getBoundingClientRect(); | ||||||
|     const canvasLeft = canvasRect.left; |     const canvasLeft = canvasRect.left; | ||||||
|     const canvasTop = canvasRect.top; |     const canvasTop = canvasRect.top; | ||||||
|     const centerX = window.innerWidth / 2 - canvasLeft; |     const centerX = window.innerWidth / 2 - canvasLeft; | ||||||
|     const centerY = window.innerHeight / 2 - canvasTop; |     const centerY = window.innerHeight / 2 - canvasTop; | ||||||
|     setScreenCenter({ x: centerX, y: centerY }); |     setScreenCenter({ x: centerX, y: centerY }); | ||||||
|   }, [applicationRef?.app?.canvas, setScreenCenter]); |   }, [applicationRef?.app, setScreenCenter]); | ||||||
|  |  | ||||||
|   const handlePointerDown = (e: FederatedMouseEvent) => { |   const handlePointerDown = (e: FederatedMouseEvent) => { | ||||||
|     setIsPointerDown(true); |     setIsPointerDown(true); | ||||||
|   | |||||||
| @@ -101,7 +101,6 @@ export function RightSidebar() { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   if (!routeData) { |   if (!routeData) { | ||||||
|     console.error("routeData is null"); |  | ||||||
|     return null; |     return null; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -166,7 +166,7 @@ export const RouteMap = observer(() => { | |||||||
|   return ( |   return ( | ||||||
|     <div style={{ width: "100%", height: "100%" }} ref={parentRef}> |     <div style={{ width: "100%", height: "100%" }} ref={parentRef}> | ||||||
|       <LanguageSwitcher /> |       <LanguageSwitcher /> | ||||||
|       <Application resizeTo={parentRef} background="#fff"> |       <Application resizeTo={parentRef} background="#fff" preference="webgl"> | ||||||
|         <InfiniteCanvas> |         <InfiniteCanvas> | ||||||
|           <TravelPath points={points} /> |           <TravelPath points={points} /> | ||||||
|           {stationData[language].map((obj, index) => ( |           {stationData[language].map((obj, index) => ( | ||||||
|   | |||||||
| @@ -1,7 +1,12 @@ | |||||||
| import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid"; | import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid"; | ||||||
| import { ruRU } from "@mui/x-data-grid/locales"; | import { ruRU } from "@mui/x-data-grid/locales"; | ||||||
| import { cityStore, languageStore, sightsStore } from "@shared"; | import { | ||||||
| import { useEffect, useState } from "react"; |   cityStore, | ||||||
|  |   languageStore, | ||||||
|  |   sightsStore, | ||||||
|  |   selectedCityStore, | ||||||
|  | } from "@shared"; | ||||||
|  | import { useEffect, useState, useMemo } from "react"; | ||||||
| import { observer } from "mobx-react-lite"; | import { observer } from "mobx-react-lite"; | ||||||
| import { Pencil, Trash2, Minus } from "lucide-react"; | import { Pencil, Trash2, Minus } from "lucide-react"; | ||||||
| import { useNavigate } from "react-router-dom"; | import { useNavigate } from "react-router-dom"; | ||||||
| @@ -68,6 +73,7 @@ export const SightListPage = observer(() => { | |||||||
|       headerName: "Действия", |       headerName: "Действия", | ||||||
|       align: "center", |       align: "center", | ||||||
|       headerAlign: "center", |       headerAlign: "center", | ||||||
|  |       sortable: false, | ||||||
|  |  | ||||||
|       renderCell: (params: GridRenderCellParams) => { |       renderCell: (params: GridRenderCellParams) => { | ||||||
|         return ( |         return ( | ||||||
| @@ -92,7 +98,16 @@ export const SightListPage = observer(() => { | |||||||
|     }, |     }, | ||||||
|   ]; |   ]; | ||||||
|  |  | ||||||
|   const rows = sights.map((sight) => ({ |   // Фильтрация достопримечательностей по выбранному городу | ||||||
|  |   const filteredSights = useMemo(() => { | ||||||
|  |     const { selectedCityId } = selectedCityStore; | ||||||
|  |     if (!selectedCityId) { | ||||||
|  |       return sights; | ||||||
|  |     } | ||||||
|  |     return sights.filter((sight: any) => sight.city_id === selectedCityId); | ||||||
|  |   }, [sights, selectedCityStore.selectedCityId]); | ||||||
|  |  | ||||||
|  |   const rows = filteredSights.map((sight) => ({ | ||||||
|     id: sight.id, |     id: sight.id, | ||||||
|     name: sight.name, |     name: sight.name, | ||||||
|     city_id: sight.city_id, |     city_id: sight.city_id, | ||||||
|   | |||||||
| @@ -2,23 +2,16 @@ import { Button, TextField } from "@mui/material"; | |||||||
| import { snapshotStore } from "@shared"; | import { snapshotStore } from "@shared"; | ||||||
| import { observer } from "mobx-react-lite"; | import { observer } from "mobx-react-lite"; | ||||||
| import { ArrowLeft, Loader2, Save } from "lucide-react"; | import { ArrowLeft, Loader2, Save } from "lucide-react"; | ||||||
| import { useEffect, useState } from "react"; | import { useState } from "react"; | ||||||
| import { useNavigate, useParams } from "react-router-dom"; | import { useNavigate } from "react-router-dom"; | ||||||
| import { toast } from "react-toastify"; | import { toast } from "react-toastify"; | ||||||
|  |  | ||||||
| export const SnapshotCreatePage = observer(() => { | export const SnapshotCreatePage = observer(() => { | ||||||
|   const { id } = useParams(); |   const { createSnapshot } = snapshotStore; | ||||||
|   const { getSnapshot, createSnapshot } = snapshotStore; |  | ||||||
|   const navigate = useNavigate(); |   const navigate = useNavigate(); | ||||||
|   const [name, setName] = useState(""); |   const [name, setName] = useState(""); | ||||||
|   const [isLoading, setIsLoading] = useState(false); |   const [isLoading, setIsLoading] = useState(false); | ||||||
|  |  | ||||||
|   useEffect(() => { |  | ||||||
|     (async () => { |  | ||||||
|       await getSnapshot(id as string); |  | ||||||
|     })(); |  | ||||||
|   }, [id]); |  | ||||||
|  |  | ||||||
|   return ( |   return ( | ||||||
|     <div className="w-full h-[400px] flex justify-center items-center"> |     <div className="w-full h-[400px] flex justify-center items-center"> | ||||||
|       <div className="w-full h-full p-3 flex flex-col gap-10"> |       <div className="w-full h-full p-3 flex flex-col gap-10"> | ||||||
|   | |||||||
| @@ -43,6 +43,7 @@ export const SnapshotListPage = observer(() => { | |||||||
|       headerName: "Действия", |       headerName: "Действия", | ||||||
|       width: 300, |       width: 300, | ||||||
|       headerAlign: "center", |       headerAlign: "center", | ||||||
|  |       sortable: false, | ||||||
|  |  | ||||||
|       renderCell: (params: GridRenderCellParams) => { |       renderCell: (params: GridRenderCellParams) => { | ||||||
|         return ( |         return ( | ||||||
|   | |||||||
| @@ -17,9 +17,10 @@ import { | |||||||
|   Paper, |   Paper, | ||||||
|   TableBody, |   TableBody, | ||||||
| } from "@mui/material"; | } from "@mui/material"; | ||||||
|  | import { observer } from "mobx-react-lite"; | ||||||
| import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; | import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; | ||||||
|  |  | ||||||
| import { authInstance, languageStore } from "@shared"; | import { authInstance, languageStore, selectedCityStore } from "@shared"; | ||||||
|  |  | ||||||
| type Field<T> = { | type Field<T> = { | ||||||
|   label: string; |   label: string; | ||||||
| @@ -73,7 +74,7 @@ export const LinkedSights = < | |||||||
|   ); |   ); | ||||||
| }; | }; | ||||||
|  |  | ||||||
| export const LinkedSightsContents = < | const LinkedSightsContentsInner = < | ||||||
|   T extends { id: number; name: string; [key: string]: any } |   T extends { id: number; name: string; [key: string]: any } | ||||||
| >({ | >({ | ||||||
|   parentId, |   parentId, | ||||||
| @@ -100,6 +101,14 @@ export const LinkedSightsContents = < | |||||||
|  |  | ||||||
|   const availableItems = allItems |   const availableItems = allItems | ||||||
|     .filter((item) => !linkedItems.some((linked) => linked.id === item.id)) |     .filter((item) => !linkedItems.some((linked) => linked.id === item.id)) | ||||||
|  |     .filter((item) => { | ||||||
|  |       // Фильтруем по городу из навбара | ||||||
|  |       const selectedCityId = selectedCityStore.selectedCityId; | ||||||
|  |       if (selectedCityId && "city_id" in item) { | ||||||
|  |         return item.city_id === selectedCityId; | ||||||
|  |       } | ||||||
|  |       return true; | ||||||
|  |     }) | ||||||
|     .sort((a, b) => a.name.localeCompare(b.name)); |     .sort((a, b) => a.name.localeCompare(b.name)); | ||||||
|  |  | ||||||
|   useEffect(() => { |   useEffect(() => { | ||||||
| @@ -313,3 +322,7 @@ export const LinkedSightsContents = < | |||||||
|     </> |     </> | ||||||
|   ); |   ); | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | export const LinkedSightsContents = observer( | ||||||
|  |   LinkedSightsContentsInner | ||||||
|  | ) as typeof LinkedSightsContentsInner; | ||||||
|   | |||||||
| @@ -68,7 +68,10 @@ export const StationEditPage = observer(() => { | |||||||
|   const handleEdit = async () => { |   const handleEdit = async () => { | ||||||
|     const isCityMissing = !editStationData.common.city_id; |     const isCityMissing = !editStationData.common.city_id; | ||||||
|     // Проверяем названия на всех языках |     // Проверяем названия на всех языках | ||||||
|     const isNameMissing = !editStationData.ru.name || !editStationData.en.name || !editStationData.zh.name; |     const isNameMissing = | ||||||
|  |       !editStationData.ru.name || | ||||||
|  |       !editStationData.en.name || | ||||||
|  |       !editStationData.zh.name; | ||||||
|  |  | ||||||
|     if (isCityMissing || isNameMissing) { |     if (isCityMissing || isNameMissing) { | ||||||
|       setIsSaveWarningOpen(true); |       setIsSaveWarningOpen(true); | ||||||
| @@ -106,6 +109,7 @@ export const StationEditPage = observer(() => { | |||||||
|   return ( |   return ( | ||||||
|     <Paper className="w-full h-full p-3 flex flex-col gap-10"> |     <Paper className="w-full h-full p-3 flex flex-col gap-10"> | ||||||
|       <LanguageSwitcher /> |       <LanguageSwitcher /> | ||||||
|  |  | ||||||
|       <div className="flex items-center gap-4"> |       <div className="flex items-center gap-4"> | ||||||
|         <button |         <button | ||||||
|           className="flex items-center gap-2" |           className="flex items-center gap-2" | ||||||
|   | |||||||
| @@ -1,6 +1,11 @@ | |||||||
| import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid"; | import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid"; | ||||||
| import { ruRU } from "@mui/x-data-grid/locales"; | import { ruRU } from "@mui/x-data-grid/locales"; | ||||||
| import { languageStore, stationsStore } from "@shared"; | import { | ||||||
|  |   languageStore, | ||||||
|  |   stationsStore, | ||||||
|  |   selectedCityStore, | ||||||
|  |   cityStore, | ||||||
|  | } from "@shared"; | ||||||
| import { useEffect, useState } from "react"; | import { useEffect, useState } from "react"; | ||||||
| import { observer } from "mobx-react-lite"; | import { observer } from "mobx-react-lite"; | ||||||
| import { Eye, Pencil, Trash2, Minus } from "lucide-react"; | import { Eye, Pencil, Trash2, Minus } from "lucide-react"; | ||||||
| @@ -21,6 +26,7 @@ export const StationListPage = observer(() => { | |||||||
|   useEffect(() => { |   useEffect(() => { | ||||||
|     const fetchStations = async () => { |     const fetchStations = async () => { | ||||||
|       setIsLoading(true); |       setIsLoading(true); | ||||||
|  |       await cityStore.getCities(language); | ||||||
|       await getStationList(); |       await getStationList(); | ||||||
|       setIsLoading(false); |       setIsLoading(false); | ||||||
|     }; |     }; | ||||||
| @@ -85,6 +91,7 @@ export const StationListPage = observer(() => { | |||||||
|       width: 140, |       width: 140, | ||||||
|       align: "center", |       align: "center", | ||||||
|       headerAlign: "center", |       headerAlign: "center", | ||||||
|  |       sortable: false, | ||||||
|  |  | ||||||
|       renderCell: (params: GridRenderCellParams) => { |       renderCell: (params: GridRenderCellParams) => { | ||||||
|         return ( |         return ( | ||||||
| @@ -109,7 +116,18 @@ export const StationListPage = observer(() => { | |||||||
|     }, |     }, | ||||||
|   ]; |   ]; | ||||||
|  |  | ||||||
|   const rows = stationLists[language].data.map((station: any) => ({ |   // Фильтрация станций по выбранному городу | ||||||
|  |   const filteredStations = () => { | ||||||
|  |     const { selectedCityId } = selectedCityStore; | ||||||
|  |     if (!selectedCityId) { | ||||||
|  |       return stationLists[language].data; | ||||||
|  |     } | ||||||
|  |     return stationLists[language].data.filter( | ||||||
|  |       (station: any) => station.city_id === selectedCityId | ||||||
|  |     ); | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   const rows = filteredStations().map((station: any) => ({ | ||||||
|     id: station.id, |     id: station.id, | ||||||
|     name: station.name, |     name: station.name, | ||||||
|     system_name: station.system_name, |     system_name: station.system_name, | ||||||
|   | |||||||
| @@ -83,6 +83,7 @@ export const UserListPage = observer(() => { | |||||||
|       flex: 1, |       flex: 1, | ||||||
|       align: "center", |       align: "center", | ||||||
|       headerAlign: "center", |       headerAlign: "center", | ||||||
|  |       sortable: false, | ||||||
|  |  | ||||||
|       renderCell: (params: GridRenderCellParams) => { |       renderCell: (params: GridRenderCellParams) => { | ||||||
|         return ( |         return ( | ||||||
|   | |||||||
| @@ -102,6 +102,7 @@ export const VehicleListPage = observer(() => { | |||||||
|       width: 200, |       width: 200, | ||||||
|       align: "center", |       align: "center", | ||||||
|       headerAlign: "center", |       headerAlign: "center", | ||||||
|  |       sortable: false, | ||||||
|  |  | ||||||
|       renderCell: (params: GridRenderCellParams) => { |       renderCell: (params: GridRenderCellParams) => { | ||||||
|         return ( |         return ( | ||||||
|   | |||||||
| @@ -2,7 +2,7 @@ import { languageStore, Language } from "@shared"; | |||||||
| import axios, { AxiosError, InternalAxiosRequestConfig } from "axios"; | import axios, { AxiosError, InternalAxiosRequestConfig } from "axios"; | ||||||
|  |  | ||||||
| const authInstance = axios.create({ | const authInstance = axios.create({ | ||||||
|   baseURL: "https://wn.krbl.ru", |   baseURL: import.meta.env.VITE_API_URL, | ||||||
| }); | }); | ||||||
|  |  | ||||||
| authInstance.interceptors.request.use((config: InternalAxiosRequestConfig) => { | authInstance.interceptors.request.use((config: InternalAxiosRequestConfig) => { | ||||||
| @@ -24,7 +24,7 @@ authInstance.interceptors.response.use( | |||||||
|  |  | ||||||
| const languageInstance = (language: Language) => { | const languageInstance = (language: Language) => { | ||||||
|   const instance = axios.create({ |   const instance = axios.create({ | ||||||
|     baseURL: "https://wn.krbl.ru", |     baseURL: import.meta.env.VITE_API_URL, | ||||||
|   }); |   }); | ||||||
|   instance.interceptors.request.use((config) => { |   instance.interceptors.request.use((config) => { | ||||||
|     config.headers.Authorization = `Bearer ${localStorage.getItem("token")}`; |     config.headers.Authorization = `Bearer ${localStorage.getItem("token")}`; | ||||||
|   | |||||||
| @@ -25,6 +25,7 @@ interface NavigationItem { | |||||||
|   label: string; |   label: string; | ||||||
|   icon?: LucideIcon | React.ReactNode; |   icon?: LucideIcon | React.ReactNode; | ||||||
|   path?: string; |   path?: string; | ||||||
|  |   for_admin?: boolean; | ||||||
|   onClick?: () => void; |   onClick?: () => void; | ||||||
|   nestedItems?: NavigationItem[]; |   nestedItems?: NavigationItem[]; | ||||||
|   isActive?: boolean; |   isActive?: boolean; | ||||||
| @@ -40,6 +41,7 @@ export const NAVIGATION_ITEMS: { | |||||||
|       label: "Снапшоты", |       label: "Снапшоты", | ||||||
|       icon: GitBranch, |       icon: GitBranch, | ||||||
|       path: "/snapshot", |       path: "/snapshot", | ||||||
|  |       for_admin: true, | ||||||
|     }, |     }, | ||||||
|     { |     { | ||||||
|       id: "map", |       id: "map", | ||||||
| @@ -52,6 +54,7 @@ export const NAVIGATION_ITEMS: { | |||||||
|       label: "Устройства", |       label: "Устройства", | ||||||
|       icon: Cpu, |       icon: Cpu, | ||||||
|       path: "/devices", |       path: "/devices", | ||||||
|  |       for_admin: true, | ||||||
|     }, |     }, | ||||||
|     // { |     // { | ||||||
|     //   id: "vehicles", |     //   id: "vehicles", | ||||||
| @@ -64,6 +67,7 @@ export const NAVIGATION_ITEMS: { | |||||||
|       label: "Пользователи", |       label: "Пользователи", | ||||||
|       icon: Users, |       icon: Users, | ||||||
|       path: "/user", |       path: "/user", | ||||||
|  |       for_admin: true, | ||||||
|     }, |     }, | ||||||
|     { |     { | ||||||
|       id: "all", |       id: "all", | ||||||
| @@ -106,12 +110,14 @@ export const NAVIGATION_ITEMS: { | |||||||
|           label: "Страны", |           label: "Страны", | ||||||
|           icon: Earth, |           icon: Earth, | ||||||
|           path: "/country", |           path: "/country", | ||||||
|  |           for_admin: true, | ||||||
|         }, |         }, | ||||||
|         { |         { | ||||||
|           id: "cities", |           id: "cities", | ||||||
|           label: "Города", |           label: "Города", | ||||||
|           icon: Building2, |           icon: Building2, | ||||||
|           path: "/city", |           path: "/city", | ||||||
|  |           for_admin: true, | ||||||
|         }, |         }, | ||||||
|         { |         { | ||||||
|           id: "carriers", |           id: "carriers", | ||||||
| @@ -119,6 +125,7 @@ export const NAVIGATION_ITEMS: { | |||||||
|           // @ts-ignore |           // @ts-ignore | ||||||
|           icon: CarrierSvg, |           icon: CarrierSvg, | ||||||
|           path: "/carrier", |           path: "/carrier", | ||||||
|  |           for_admin: true, | ||||||
|         }, |         }, | ||||||
|       ], |       ], | ||||||
|     }, |     }, | ||||||
|   | |||||||
| @@ -1,4 +1,4 @@ | |||||||
| export const API_URL = "https://wn.krbl.ru"; | export const API_URL = import.meta.env.VITE_API_URL; | ||||||
| export const MEDIA_TYPE_LABELS = { | export const MEDIA_TYPE_LABELS = { | ||||||
|   1: "Фото", |   1: "Фото", | ||||||
|   2: "Видео", |   2: "Видео", | ||||||
| @@ -8,6 +8,8 @@ export const MEDIA_TYPE_LABELS = { | |||||||
|   6: "3Д-модель", |   6: "3Д-модель", | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | export * from "./mediaTypes"; | ||||||
|  |  | ||||||
| export const MEDIA_TYPE_VALUES = { | export const MEDIA_TYPE_VALUES = { | ||||||
|   image: 1, |   image: 1, | ||||||
|   video: 2, |   video: 2, | ||||||
|   | |||||||
							
								
								
									
										85
									
								
								src/shared/const/mediaTypes.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										85
									
								
								src/shared/const/mediaTypes.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,85 @@ | |||||||
|  | // Допустимые типы и расширения файлов для медиа | ||||||
|  | export const ALLOWED_MEDIA_TYPES = { | ||||||
|  |   image: { | ||||||
|  |     extensions: [".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp", ".svg"], | ||||||
|  |     mimeTypes: [ | ||||||
|  |       "image/jpeg", | ||||||
|  |       "image/png", | ||||||
|  |       "image/gif", | ||||||
|  |       "image/webp", | ||||||
|  |       "image/bmp", | ||||||
|  |       "image/svg+xml", | ||||||
|  |     ], | ||||||
|  |     accept: "image/*", | ||||||
|  |   }, | ||||||
|  |   video: { | ||||||
|  |     extensions: [".mp4", ".webm", ".ogg", ".mov", ".avi"], | ||||||
|  |     mimeTypes: [ | ||||||
|  |       "video/mp4", | ||||||
|  |       "video/webm", | ||||||
|  |       "video/ogg", | ||||||
|  |       "video/quicktime", | ||||||
|  |       "video/x-msvideo", | ||||||
|  |     ], | ||||||
|  |     accept: "video/*", | ||||||
|  |   }, | ||||||
|  |   model3d: { | ||||||
|  |     extensions: [".glb", ".gltf"], | ||||||
|  |     mimeTypes: ["model/gltf-binary", "model/gltf+json"], | ||||||
|  |     accept: ".glb,.gltf", | ||||||
|  |   }, | ||||||
|  |   panorama: { | ||||||
|  |     extensions: [".jpg", ".jpeg", ".png"], | ||||||
|  |     mimeTypes: ["image/jpeg", "image/png"], | ||||||
|  |     accept: "image/*", | ||||||
|  |   }, | ||||||
|  | } as const; | ||||||
|  |  | ||||||
|  | export const getAllAllowedExtensions = (): string[] => { | ||||||
|  |   return [ | ||||||
|  |     ...ALLOWED_MEDIA_TYPES.image.extensions, | ||||||
|  |     ...ALLOWED_MEDIA_TYPES.video.extensions, | ||||||
|  |     ...ALLOWED_MEDIA_TYPES.model3d.extensions, | ||||||
|  |   ]; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export const getAllAcceptString = (): string => { | ||||||
|  |   return `${ALLOWED_MEDIA_TYPES.image.accept},${ALLOWED_MEDIA_TYPES.video.accept},${ALLOWED_MEDIA_TYPES.model3d.accept}`; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export const validateFileExtension = ( | ||||||
|  |   file: File | ||||||
|  | ): { valid: boolean; error?: string } => { | ||||||
|  |   const fileName = file.name.toLowerCase(); | ||||||
|  |   const extension = fileName.substring(fileName.lastIndexOf(".")); | ||||||
|  |   const allowedExtensions = getAllAllowedExtensions(); | ||||||
|  |  | ||||||
|  |   if (!allowedExtensions.includes(extension)) { | ||||||
|  |     return { | ||||||
|  |       valid: false, | ||||||
|  |       error: `Неподдерживаемый формат файла "${extension}". Допустимые форматы: ${allowedExtensions.join( | ||||||
|  |         ", " | ||||||
|  |       )}`, | ||||||
|  |     }; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   return { valid: true }; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export const filterValidFiles = ( | ||||||
|  |   files: File[] | ||||||
|  | ): { validFiles: File[]; errors: string[] } => { | ||||||
|  |   const validFiles: File[] = []; | ||||||
|  |   const errors: string[] = []; | ||||||
|  |  | ||||||
|  |   files.forEach((file) => { | ||||||
|  |     const validation = validateFileExtension(file); | ||||||
|  |     if (validation.valid) { | ||||||
|  |       validFiles.push(file); | ||||||
|  |     } else { | ||||||
|  |       errors.push(`${file.name}: ${validation.error}`); | ||||||
|  |     } | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   return { validFiles, errors }; | ||||||
|  | }; | ||||||
							
								
								
									
										82
									
								
								src/shared/lib/gltfCacheManager.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										82
									
								
								src/shared/lib/gltfCacheManager.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,82 @@ | |||||||
|  | /** | ||||||
|  |  * Утилита для управления кешем GLTF и blob URL | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | // Динамический импорт useGLTF для избежания проблем с SSR | ||||||
|  | let useGLTF: any = null; | ||||||
|  |  | ||||||
|  | const initializeUseGLTF = async () => { | ||||||
|  |   if (!useGLTF) { | ||||||
|  |     try { | ||||||
|  |       const drei = await import("@react-three/drei"); | ||||||
|  |       useGLTF = drei.useGLTF; | ||||||
|  |     } catch (error) { | ||||||
|  |       console.warn( | ||||||
|  |         "⚠️ GLTFCacheManager: Не удалось импортировать useGLTF", | ||||||
|  |         error | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |   return useGLTF; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Очищает кеш GLTF для конкретного URL | ||||||
|  |  */ | ||||||
|  | export const clearGLTFCacheForUrl = async (url: string) => { | ||||||
|  |   try { | ||||||
|  |     const gltf = await initializeUseGLTF(); | ||||||
|  |     if (gltf && gltf.clear) { | ||||||
|  |       gltf.clear(url); | ||||||
|  |     } | ||||||
|  |   } catch (error) {} | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Очищает весь кеш GLTF | ||||||
|  |  */ | ||||||
|  | export const clearAllGLTFCache = async () => { | ||||||
|  |   try { | ||||||
|  |     const gltf = await initializeUseGLTF(); | ||||||
|  |     if (gltf && gltf.clear) { | ||||||
|  |       gltf.clear(); | ||||||
|  |     } | ||||||
|  |   } catch (error) {} | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Очищает blob URL из памяти браузера | ||||||
|  |  */ | ||||||
|  | export const revokeBlobURL = (url: string) => { | ||||||
|  |   if (url && url.startsWith("blob:")) { | ||||||
|  |     try { | ||||||
|  |       URL.revokeObjectURL(url); | ||||||
|  |     } catch (error) {} | ||||||
|  |   } | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Комплексная очистка: blob URL + кеш GLTF | ||||||
|  |  */ | ||||||
|  | export const clearBlobAndGLTFCache = async (url: string) => { | ||||||
|  |   // Сначала отзываем blob URL | ||||||
|  |   revokeBlobURL(url); | ||||||
|  |  | ||||||
|  |   // Затем очищаем кеш GLTF | ||||||
|  |   await clearGLTFCacheForUrl(url); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Очистка при смене медиа (для предотвращения конфликтов) | ||||||
|  |  */ | ||||||
|  | export const clearMediaTransitionCache = async ( | ||||||
|  |   previousMediaId: string | number | null, | ||||||
|  |   newMediaId: string | number | null, | ||||||
|  |   newMediaType?: number | ||||||
|  | ) => { | ||||||
|  |   console.log(newMediaId, newMediaType); | ||||||
|  |   // Если переключаемся с/на 3D модель, очищаем весь кеш | ||||||
|  |   if (newMediaType === 6 || previousMediaId) { | ||||||
|  |     await clearAllGLTFCache(); | ||||||
|  |   } | ||||||
|  | }; | ||||||
| @@ -1,5 +1,6 @@ | |||||||
| export * from "./mui/theme"; | export * from "./mui/theme"; | ||||||
| export * from "./DecodeJWT"; | export * from "./DecodeJWT"; | ||||||
|  | export * from "./gltfCacheManager"; | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * Генерирует название медиа по умолчанию в разных форматах |  * Генерирует название медиа по умолчанию в разных форматах | ||||||
|   | |||||||
| @@ -3,9 +3,10 @@ import { | |||||||
|   MEDIA_TYPE_VALUES, |   MEDIA_TYPE_VALUES, | ||||||
|   editSightStore, |   editSightStore, | ||||||
|   generateDefaultMediaName, |   generateDefaultMediaName, | ||||||
|  |   clearBlobAndGLTFCache, | ||||||
| } from "@shared"; | } from "@shared"; | ||||||
| import { observer } from "mobx-react-lite"; | import { observer } from "mobx-react-lite"; | ||||||
| import { useEffect, useState } from "react"; | import { useEffect, useState, useRef } from "react"; | ||||||
| import { | import { | ||||||
|   Dialog, |   Dialog, | ||||||
|   DialogTitle, |   DialogTitle, | ||||||
| @@ -82,18 +83,41 @@ export const UploadMediaDialog = observer( | |||||||
|       [] |       [] | ||||||
|     ); |     ); | ||||||
|     const [isPreviewLoaded, setIsPreviewLoaded] = useState(false); |     const [isPreviewLoaded, setIsPreviewLoaded] = useState(false); | ||||||
|  |     const previousMediaUrlRef = useRef<string | null>(null); | ||||||
|  |  | ||||||
|     useEffect(() => { |     useEffect(() => { | ||||||
|       if (initialFile) { |       if (initialFile) { | ||||||
|  |         // Очищаем предыдущий blob URL если он существует | ||||||
|  |         if ( | ||||||
|  |           previousMediaUrlRef.current && | ||||||
|  |           previousMediaUrlRef.current.startsWith("blob:") | ||||||
|  |         ) { | ||||||
|  |           clearBlobAndGLTFCache(previousMediaUrlRef.current); | ||||||
|  |         } | ||||||
|  |  | ||||||
|         setMediaFile(initialFile); |         setMediaFile(initialFile); | ||||||
|         setMediaFilename(initialFile.name); |         setMediaFilename(initialFile.name); | ||||||
|         setAvailableMediaTypes([2]); |         setAvailableMediaTypes([2]); | ||||||
|         setMediaType(2); |         setMediaType(2); | ||||||
|         setMediaUrl(URL.createObjectURL(initialFile)); |         const newBlobUrl = URL.createObjectURL(initialFile); | ||||||
|  |         setMediaUrl(newBlobUrl); | ||||||
|  |         previousMediaUrlRef.current = newBlobUrl; | ||||||
|         setMediaName(initialFile.name.replace(/\.[^/.]+$/, "")); |         setMediaName(initialFile.name.replace(/\.[^/.]+$/, "")); | ||||||
|       } |       } | ||||||
|     }, [initialFile]); |     }, [initialFile]); | ||||||
|  |  | ||||||
|  |     // Очистка blob URL при размонтировании компонента | ||||||
|  |     useEffect(() => { | ||||||
|  |       return () => { | ||||||
|  |         if ( | ||||||
|  |           previousMediaUrlRef.current && | ||||||
|  |           previousMediaUrlRef.current.startsWith("blob:") | ||||||
|  |         ) { | ||||||
|  |           clearBlobAndGLTFCache(previousMediaUrlRef.current); | ||||||
|  |         } | ||||||
|  |       }; | ||||||
|  |     }, []); // Пустой массив зависимостей - выполняется только при размонтировании | ||||||
|  |  | ||||||
|     useEffect(() => { |     useEffect(() => { | ||||||
|       if (fileToUpload) { |       if (fileToUpload) { | ||||||
|         setMediaFile(fileToUpload); |         setMediaFile(fileToUpload); | ||||||
| @@ -105,7 +129,11 @@ export const UploadMediaDialog = observer( | |||||||
|             setAvailableMediaTypes([6]); |             setAvailableMediaTypes([6]); | ||||||
|             setMediaType(6); |             setMediaType(6); | ||||||
|           } |           } | ||||||
|           if (["jpg", "jpeg", "png", "gif", "svg"].includes(extension)) { |           if ( | ||||||
|  |             ["jpg", "jpeg", "png", "gif", "svg", "webp", "bmp"].includes( | ||||||
|  |               extension | ||||||
|  |             ) | ||||||
|  |           ) { | ||||||
|             // Для изображений доступны все типы кроме видео |             // Для изображений доступны все типы кроме видео | ||||||
|             setAvailableMediaTypes([1, 3, 4, 5]); // Фото, Иконка, Водяной знак, Панорама, 3Д-модель |             setAvailableMediaTypes([1, 3, 4, 5]); // Фото, Иконка, Водяной знак, Панорама, 3Д-модель | ||||||
|             setMediaType(1); // По умолчанию Фото |             setMediaType(1); // По умолчанию Фото | ||||||
| @@ -207,10 +235,20 @@ export const UploadMediaDialog = observer( | |||||||
|  |  | ||||||
|     useEffect(() => { |     useEffect(() => { | ||||||
|       if (mediaFile) { |       if (mediaFile) { | ||||||
|         setMediaUrl(URL.createObjectURL(mediaFile as Blob)); |         // Очищаем предыдущий blob URL и кеш GLTF если он существует | ||||||
|  |         if ( | ||||||
|  |           previousMediaUrlRef.current && | ||||||
|  |           previousMediaUrlRef.current.startsWith("blob:") | ||||||
|  |         ) { | ||||||
|  |           clearBlobAndGLTFCache(previousMediaUrlRef.current); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         const newBlobUrl = URL.createObjectURL(mediaFile as Blob); | ||||||
|  |         setMediaUrl(newBlobUrl); | ||||||
|  |         previousMediaUrlRef.current = newBlobUrl; // Сохраняем новый URL в ref | ||||||
|         setIsPreviewLoaded(false); // Сбрасываем состояние загрузки при смене файла |         setIsPreviewLoaded(false); // Сбрасываем состояние загрузки при смене файла | ||||||
|       } |       } | ||||||
|     }, [mediaFile]); |     }, [mediaFile]); // Убираем mediaUrl из зависимостей чтобы избежать зацикливания | ||||||
|  |  | ||||||
|     // const fileFormat = useEffect(() => { |     // const fileFormat = useEffect(() => { | ||||||
|     //   const handleKeyPress = (event: KeyboardEvent) => { |     //   const handleKeyPress = (event: KeyboardEvent) => { | ||||||
| @@ -259,8 +297,20 @@ export const UploadMediaDialog = observer( | |||||||
|     }; |     }; | ||||||
|  |  | ||||||
|     const handleClose = () => { |     const handleClose = () => { | ||||||
|  |       // Очищаем blob URL и кеш GLTF при закрытии диалога | ||||||
|  |       if ( | ||||||
|  |         previousMediaUrlRef.current && | ||||||
|  |         previousMediaUrlRef.current.startsWith("blob:") | ||||||
|  |       ) { | ||||||
|  |         clearBlobAndGLTFCache(previousMediaUrlRef.current); | ||||||
|  |       } | ||||||
|  |  | ||||||
|       setError(null); |       setError(null); | ||||||
|       setSuccess(false); |       setSuccess(false); | ||||||
|  |       setMediaUrl(null); | ||||||
|  |       setMediaFile(null); | ||||||
|  |       setIsPreviewLoaded(false); | ||||||
|  |       previousMediaUrlRef.current = null; // Очищаем ref | ||||||
|       onClose(); |       onClose(); | ||||||
|     }; |     }; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -551,7 +551,6 @@ class CreateSightStore { | |||||||
|       }); |       }); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     console.log("Sight created with ID:", newSightId); |  | ||||||
|     // Optionally: this.clearCreateSight(); // To reset form after successful creation |     // Optionally: this.clearCreateSight(); // To reset form after successful creation | ||||||
|     this.needLeaveAgree = false; |     this.needLeaveAgree = false; | ||||||
|     return newSightId; |     return newSightId; | ||||||
|   | |||||||
							
								
								
									
										101
									
								
								src/shared/store/ModelLoadingStore/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										101
									
								
								src/shared/store/ModelLoadingStore/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,101 @@ | |||||||
|  | import { makeAutoObservable } from "mobx"; | ||||||
|  |  | ||||||
|  | export interface ModelLoadingState { | ||||||
|  |   isLoading: boolean; | ||||||
|  |   progress: number; | ||||||
|  |   modelId: string | null; | ||||||
|  |   error?: string; | ||||||
|  |   startTime?: number; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | class ModelLoadingStore { | ||||||
|  |   private loadingStates: Map<string, ModelLoadingState> = new Map(); | ||||||
|  |  | ||||||
|  |   constructor() { | ||||||
|  |     makeAutoObservable(this); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // Начать отслеживание загрузки модели | ||||||
|  |   startLoading(modelId: string) { | ||||||
|  |     this.loadingStates.set(modelId, { | ||||||
|  |       isLoading: true, | ||||||
|  |       progress: 0, | ||||||
|  |       modelId, | ||||||
|  |       startTime: Date.now(), | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // Обновить прогресс загрузки | ||||||
|  |   updateProgress(modelId: string, progress: number) { | ||||||
|  |     const state = this.loadingStates.get(modelId); | ||||||
|  |     if (state) { | ||||||
|  |       state.progress = Math.min(100, Math.max(0, progress)); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // Завершить загрузку модели | ||||||
|  |   finishLoading(modelId: string) { | ||||||
|  |     const state = this.loadingStates.get(modelId); | ||||||
|  |     if (state) { | ||||||
|  |       state.isLoading = false; | ||||||
|  |       state.progress = 100; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // Остановить загрузку (в случае ошибки) | ||||||
|  |   stopLoading(modelId: string) { | ||||||
|  |     this.loadingStates.delete(modelId); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // Обработать ошибку загрузки | ||||||
|  |   handleError(modelId: string, error?: string) { | ||||||
|  |     const state = this.loadingStates.get(modelId); | ||||||
|  |     if (state) { | ||||||
|  |       state.isLoading = false; | ||||||
|  |       state.error = error || "Ошибка загрузки модели"; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // Получить состояние загрузки для конкретной модели | ||||||
|  |   getLoadingState(modelId: string): ModelLoadingState | undefined { | ||||||
|  |     return this.loadingStates.get(modelId); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // Проверить, загружается ли какая-либо модель | ||||||
|  |   get isAnyModelLoading(): boolean { | ||||||
|  |     return Array.from(this.loadingStates.values()).some( | ||||||
|  |       (state) => state.isLoading | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // Получить все загружающиеся модели | ||||||
|  |   get loadingModels(): ModelLoadingState[] { | ||||||
|  |     return Array.from(this.loadingStates.values()).filter( | ||||||
|  |       (state) => state.isLoading | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // Получить общий прогресс всех загружающихся моделей | ||||||
|  |   get overallProgress(): number { | ||||||
|  |     const loadingModels = this.loadingModels; | ||||||
|  |     if (loadingModels.length === 0) return 100; | ||||||
|  |  | ||||||
|  |     const totalProgress = loadingModels.reduce( | ||||||
|  |       (sum, model) => sum + model.progress, | ||||||
|  |       0 | ||||||
|  |     ); | ||||||
|  |     return Math.round(totalProgress / loadingModels.length); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // Проверить, заблокировано ли сохранение (есть ли загружающиеся модели) | ||||||
|  |   get isSaveBlocked(): boolean { | ||||||
|  |     return this.isAnyModelLoading; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // Очистить все состояния загрузки | ||||||
|  |   clearAll() { | ||||||
|  |     this.loadingStates.clear(); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export const modelLoadingStore = new ModelLoadingStore(); | ||||||
| @@ -134,7 +134,6 @@ class RouteStore { | |||||||
|  |  | ||||||
|   copyRouteAction = async (id: number) => { |   copyRouteAction = async (id: number) => { | ||||||
|     const response = await authInstance.post(`/route/${id}/copy`); |     const response = await authInstance.post(`/route/${id}/copy`); | ||||||
|     console.log(response); |  | ||||||
|  |  | ||||||
|     runInAction(() => { |     runInAction(() => { | ||||||
|       this.routes.data = [...this.routes.data, response.data]; |       this.routes.data = [...this.routes.data, response.data]; | ||||||
|   | |||||||
| @@ -228,21 +228,13 @@ class SnapshotStore { | |||||||
|     // Попытка очистить кеш браузера (если поддерживается) |     // Попытка очистить кеш браузера (если поддерживается) | ||||||
|     if ("caches" in window) { |     if ("caches" in window) { | ||||||
|       try { |       try { | ||||||
|         caches |         caches.keys().then((cacheNames) => { | ||||||
|           .keys() |           return Promise.all( | ||||||
|           .then((cacheNames) => { |             cacheNames.map((cacheName) => { | ||||||
|             return Promise.all( |               return caches.delete(cacheName); | ||||||
|               cacheNames.map((cacheName) => { |             }) | ||||||
|                 return caches.delete(cacheName); |           ); | ||||||
|               }) |         }); | ||||||
|             ); |  | ||||||
|           }) |  | ||||||
|           .then(() => { |  | ||||||
|             console.log("Кеш браузера очищен"); |  | ||||||
|           }) |  | ||||||
|           .catch((error) => { |  | ||||||
|             console.warn("Не удалось очистить кеш браузера:", error); |  | ||||||
|           }); |  | ||||||
|       } catch (error) { |       } catch (error) { | ||||||
|         console.warn("Кеш браузера не поддерживается:", error); |         console.warn("Кеш браузера не поддерживается:", error); | ||||||
|       } |       } | ||||||
| @@ -251,30 +243,20 @@ class SnapshotStore { | |||||||
|     // Попытка очистить IndexedDB (если поддерживается) |     // Попытка очистить IndexedDB (если поддерживается) | ||||||
|     if ("indexedDB" in window) { |     if ("indexedDB" in window) { | ||||||
|       try { |       try { | ||||||
|         indexedDB |         indexedDB.databases().then((databases) => { | ||||||
|           .databases() |           return Promise.all( | ||||||
|           .then((databases) => { |             databases.map((db) => { | ||||||
|             return Promise.all( |               if (db.name) { | ||||||
|               databases.map((db) => { |                 return indexedDB.deleteDatabase(db.name); | ||||||
|                 if (db.name) { |               } | ||||||
|                   return indexedDB.deleteDatabase(db.name); |               return Promise.resolve(); | ||||||
|                 } |             }) | ||||||
|                 return Promise.resolve(); |           ); | ||||||
|               }) |         }); | ||||||
|             ); |  | ||||||
|           }) |  | ||||||
|           .then(() => { |  | ||||||
|             console.log("IndexedDB очищен"); |  | ||||||
|           }) |  | ||||||
|           .catch((error) => { |  | ||||||
|             console.warn("Не удалось очистить IndexedDB:", error); |  | ||||||
|           }); |  | ||||||
|       } catch (error) { |       } catch (error) { | ||||||
|         console.warn("IndexedDB не поддерживается:", error); |         console.warn("IndexedDB не поддерживается:", error); | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     console.log("Все кеши приложения сброшены"); |  | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
|   getSnapshots = async () => { |   getSnapshots = async () => { | ||||||
|   | |||||||
							
								
								
									
										143
									
								
								src/shared/ui/LoadingSpinner/index.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										143
									
								
								src/shared/ui/LoadingSpinner/index.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,143 @@ | |||||||
|  | import React from "react"; | ||||||
|  | import { | ||||||
|  |   Box, | ||||||
|  |   CircularProgress, | ||||||
|  |   Typography, | ||||||
|  |   LinearProgress, | ||||||
|  | } from "@mui/material"; | ||||||
|  |  | ||||||
|  | interface LoadingSpinnerProps { | ||||||
|  |   progress?: number; | ||||||
|  |   message?: string; | ||||||
|  |   size?: number; | ||||||
|  |   color?: "primary" | "secondary" | "error" | "info" | "success" | "warning"; | ||||||
|  |   variant?: "circular" | "linear"; | ||||||
|  |   showPercentage?: boolean; | ||||||
|  |   thickness?: number; | ||||||
|  |   className?: string; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export const LoadingSpinner: React.FC<LoadingSpinnerProps> = ({ | ||||||
|  |   progress, | ||||||
|  |   message = "Загрузка...", | ||||||
|  |   size = 40, | ||||||
|  |   color = "primary", | ||||||
|  |   variant = "circular", | ||||||
|  |   showPercentage = true, | ||||||
|  |   thickness = 4, | ||||||
|  |   className, | ||||||
|  | }) => { | ||||||
|  |   if (variant === "linear") { | ||||||
|  |     return ( | ||||||
|  |       <Box | ||||||
|  |         className={className} | ||||||
|  |         sx={{ | ||||||
|  |           display: "flex", | ||||||
|  |           flexDirection: "column", | ||||||
|  |           alignItems: "center", | ||||||
|  |           justifyContent: "center", | ||||||
|  |           gap: 2, | ||||||
|  |           padding: 3, | ||||||
|  |           width: "100%", | ||||||
|  |         }} | ||||||
|  |       > | ||||||
|  |         <Box sx={{ width: "100%", maxWidth: 300 }}> | ||||||
|  |           <LinearProgress | ||||||
|  |             variant={progress !== undefined ? "determinate" : "indeterminate"} | ||||||
|  |             value={progress} | ||||||
|  |             color={color} | ||||||
|  |             sx={{ | ||||||
|  |               height: 8, | ||||||
|  |               borderRadius: 4, | ||||||
|  |               backgroundColor: "rgba(0, 0, 0, 0.1)", | ||||||
|  |               "& .MuiLinearProgress-bar": { | ||||||
|  |                 borderRadius: 4, | ||||||
|  |               }, | ||||||
|  |             }} | ||||||
|  |           /> | ||||||
|  |           {showPercentage && progress !== undefined && ( | ||||||
|  |             <Typography | ||||||
|  |               variant="caption" | ||||||
|  |               color="text.secondary" | ||||||
|  |               sx={{ | ||||||
|  |                 display: "block", | ||||||
|  |                 textAlign: "center", | ||||||
|  |                 mt: 1, | ||||||
|  |                 fontSize: "0.875rem", | ||||||
|  |                 fontWeight: 500, | ||||||
|  |               }} | ||||||
|  |             > | ||||||
|  |               {`${Math.round(progress)}%`} | ||||||
|  |             </Typography> | ||||||
|  |           )} | ||||||
|  |         </Box> | ||||||
|  |         {message && ( | ||||||
|  |           <Typography variant="body2" color="text.secondary" textAlign="center"> | ||||||
|  |             {message} | ||||||
|  |           </Typography> | ||||||
|  |         )} | ||||||
|  |       </Box> | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   return ( | ||||||
|  |     <Box | ||||||
|  |       className={className} | ||||||
|  |       sx={{ | ||||||
|  |         display: "flex", | ||||||
|  |         flexDirection: "column", | ||||||
|  |         alignItems: "center", | ||||||
|  |         justifyContent: "center", | ||||||
|  |         gap: 2, | ||||||
|  |         padding: 3, | ||||||
|  |       }} | ||||||
|  |     > | ||||||
|  |       <Box sx={{ position: "relative", display: "inline-flex" }}> | ||||||
|  |         <CircularProgress | ||||||
|  |           size={size} | ||||||
|  |           color={color} | ||||||
|  |           variant={progress !== undefined ? "determinate" : "indeterminate"} | ||||||
|  |           value={progress} | ||||||
|  |           thickness={thickness} | ||||||
|  |           sx={{ | ||||||
|  |             "& .MuiCircularProgress-circle": { | ||||||
|  |               strokeLinecap: "round", | ||||||
|  |             }, | ||||||
|  |           }} | ||||||
|  |         /> | ||||||
|  |         {showPercentage && progress !== undefined && ( | ||||||
|  |           <Box | ||||||
|  |             sx={{ | ||||||
|  |               top: 0, | ||||||
|  |               left: 0, | ||||||
|  |               bottom: 0, | ||||||
|  |               right: 0, | ||||||
|  |               position: "absolute", | ||||||
|  |               display: "flex", | ||||||
|  |               alignItems: "center", | ||||||
|  |               justifyContent: "center", | ||||||
|  |             }} | ||||||
|  |           > | ||||||
|  |             <Typography | ||||||
|  |               variant="caption" | ||||||
|  |               component="div" | ||||||
|  |               color="text.secondary" | ||||||
|  |               sx={{ | ||||||
|  |                 fontSize: size * 0.25, | ||||||
|  |                 fontWeight: 600, | ||||||
|  |                 lineHeight: 1, | ||||||
|  |               }} | ||||||
|  |             > | ||||||
|  |               {`${Math.round(progress)}%`} | ||||||
|  |             </Typography> | ||||||
|  |           </Box> | ||||||
|  |         )} | ||||||
|  |       </Box> | ||||||
|  |       {message && ( | ||||||
|  |         <Typography variant="body2" color="text.secondary" textAlign="center"> | ||||||
|  |           {message} | ||||||
|  |         </Typography> | ||||||
|  |       )} | ||||||
|  |     </Box> | ||||||
|  |   ); | ||||||
|  | }; | ||||||
							
								
								
									
										196
									
								
								src/shared/ui/ModelLoadingIndicator/index.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										196
									
								
								src/shared/ui/ModelLoadingIndicator/index.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,196 @@ | |||||||
|  | import React from "react"; | ||||||
|  | import { | ||||||
|  |   Box, | ||||||
|  |   Typography, | ||||||
|  |   LinearProgress, | ||||||
|  |   CircularProgress, | ||||||
|  | } from "@mui/material"; | ||||||
|  |  | ||||||
|  | interface ModelLoadingIndicatorProps { | ||||||
|  |   progress?: number; | ||||||
|  |   message?: string; | ||||||
|  |   isVisible?: boolean; | ||||||
|  |   variant?: "overlay" | "inline"; | ||||||
|  |   size?: "small" | "medium" | "large"; | ||||||
|  |   showDetails?: boolean; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export const ModelLoadingIndicator: React.FC<ModelLoadingIndicatorProps> = ({ | ||||||
|  |   progress = 0, | ||||||
|  |   message = "Загрузка 3D модели...", | ||||||
|  |   isVisible = true, | ||||||
|  |   variant = "overlay", | ||||||
|  |   size = "medium", | ||||||
|  |   showDetails = true, | ||||||
|  | }) => { | ||||||
|  |   const sizeConfig = { | ||||||
|  |     small: { | ||||||
|  |       spinnerSize: 32, | ||||||
|  |       fontSize: "0.75rem", | ||||||
|  |       progressBarWidth: 150, | ||||||
|  |       padding: 2, | ||||||
|  |     }, | ||||||
|  |     medium: { | ||||||
|  |       spinnerSize: 48, | ||||||
|  |       fontSize: "0.875rem", | ||||||
|  |       progressBarWidth: 200, | ||||||
|  |       padding: 3, | ||||||
|  |     }, | ||||||
|  |     large: { | ||||||
|  |       spinnerSize: 64, | ||||||
|  |       fontSize: "1rem", | ||||||
|  |       progressBarWidth: 250, | ||||||
|  |       padding: 4, | ||||||
|  |     }, | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   const currentSize = sizeConfig[size]; | ||||||
|  |  | ||||||
|  |   if (!isVisible) return null; | ||||||
|  |  | ||||||
|  |   const getProgressStage = (progress: number): string => { | ||||||
|  |     if (progress < 20) return "Инициализация..."; | ||||||
|  |     if (progress < 40) return "Загрузка геометрии..."; | ||||||
|  |     if (progress < 60) return "Обработка материалов..."; | ||||||
|  |     if (progress < 80) return "Загрузка текстур..."; | ||||||
|  |     if (progress < 95) return "Финализация..."; | ||||||
|  |     if (progress === 100) return "Готово!"; | ||||||
|  |     return "Загрузка..."; | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   const content = ( | ||||||
|  |     <Box | ||||||
|  |       sx={{ | ||||||
|  |         display: "flex", | ||||||
|  |         flexDirection: "column", | ||||||
|  |         alignItems: "center", | ||||||
|  |         justifyContent: "center", | ||||||
|  |         gap: 2, | ||||||
|  |         padding: currentSize.padding, | ||||||
|  |         textAlign: "center", | ||||||
|  |         width: "100%", | ||||||
|  |       }} | ||||||
|  |     > | ||||||
|  |       {/* Крутяшка с процентами */} | ||||||
|  |       <Box sx={{ position: "relative", display: "inline-flex" }}> | ||||||
|  |         <CircularProgress | ||||||
|  |           size={currentSize.spinnerSize} | ||||||
|  |           variant="determinate" | ||||||
|  |           value={progress} | ||||||
|  |           thickness={4} | ||||||
|  |           sx={{ | ||||||
|  |             color: "primary.main", | ||||||
|  |           }} | ||||||
|  |         /> | ||||||
|  |         <Box | ||||||
|  |           sx={{ | ||||||
|  |             top: 0, | ||||||
|  |             left: 0, | ||||||
|  |             bottom: 0, | ||||||
|  |             right: 0, | ||||||
|  |             position: "absolute", | ||||||
|  |             display: "flex", | ||||||
|  |             alignItems: "center", | ||||||
|  |             justifyContent: "center", | ||||||
|  |           }} | ||||||
|  |         > | ||||||
|  |           <Typography | ||||||
|  |             variant="caption" | ||||||
|  |             component="div" | ||||||
|  |             color="text.secondary" | ||||||
|  |             sx={{ | ||||||
|  |               fontSize: currentSize.spinnerSize * 0.25, | ||||||
|  |               fontWeight: 600, | ||||||
|  |               lineHeight: 1, | ||||||
|  |             }} | ||||||
|  |           > | ||||||
|  |             {`${Math.round(progress)}%`} | ||||||
|  |           </Typography> | ||||||
|  |         </Box> | ||||||
|  |       </Box> | ||||||
|  |  | ||||||
|  |       {/* Линейный прогресс бар */} | ||||||
|  |       <Box sx={{ width: "100%", maxWidth: currentSize.progressBarWidth }}> | ||||||
|  |         <LinearProgress | ||||||
|  |           variant="determinate" | ||||||
|  |           value={progress} | ||||||
|  |           color="primary" | ||||||
|  |           sx={{ | ||||||
|  |             height: 8, | ||||||
|  |             borderRadius: 4, | ||||||
|  |             backgroundColor: "rgba(0, 0, 0, 0.1)", | ||||||
|  |           }} | ||||||
|  |         /> | ||||||
|  |       </Box> | ||||||
|  |  | ||||||
|  |       {/* Основное сообщение */} | ||||||
|  |       <Typography | ||||||
|  |         variant="body2" | ||||||
|  |         color="text.secondary" | ||||||
|  |         sx={{ | ||||||
|  |           fontSize: currentSize.fontSize, | ||||||
|  |           fontWeight: 500, | ||||||
|  |           maxWidth: 280, | ||||||
|  |           lineHeight: 1.4, | ||||||
|  |         }} | ||||||
|  |       > | ||||||
|  |         {message} | ||||||
|  |       </Typography> | ||||||
|  |  | ||||||
|  |       {/* Детальная информация о прогрессе */} | ||||||
|  |       {showDetails && progress > 0 && ( | ||||||
|  |         <Typography | ||||||
|  |           variant="caption" | ||||||
|  |           color="text.disabled" | ||||||
|  |           sx={{ | ||||||
|  |             fontSize: "0.75rem", | ||||||
|  |             opacity: 0.8, | ||||||
|  |             fontWeight: 400, | ||||||
|  |           }} | ||||||
|  |         > | ||||||
|  |           {getProgressStage(progress)} | ||||||
|  |         </Typography> | ||||||
|  |       )} | ||||||
|  |     </Box> | ||||||
|  |   ); | ||||||
|  |  | ||||||
|  |   if (variant === "overlay") { | ||||||
|  |     return ( | ||||||
|  |       <Box | ||||||
|  |         sx={{ | ||||||
|  |           position: "absolute", | ||||||
|  |           top: 0, | ||||||
|  |           left: 0, | ||||||
|  |           right: 0, | ||||||
|  |           bottom: 0, | ||||||
|  |           backgroundColor: "rgba(255, 255, 255, 0.95)", | ||||||
|  |           display: "flex", | ||||||
|  |           alignItems: "center", | ||||||
|  |           justifyContent: "center", | ||||||
|  |           zIndex: 1000, | ||||||
|  |           borderRadius: 1, | ||||||
|  |           border: "1px solid rgba(0, 0, 0, 0.05)", | ||||||
|  |         }} | ||||||
|  |       > | ||||||
|  |         {content} | ||||||
|  |       </Box> | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   return ( | ||||||
|  |     <Box | ||||||
|  |       sx={{ | ||||||
|  |         display: "flex", | ||||||
|  |         alignItems: "center", | ||||||
|  |         justifyContent: "center", | ||||||
|  |         minHeight: 200, | ||||||
|  |         backgroundColor: "rgba(0, 0, 0, 0.02)", | ||||||
|  |         borderRadius: 2, | ||||||
|  |         border: "1px dashed", | ||||||
|  |         borderColor: "divider", | ||||||
|  |       }} | ||||||
|  |     > | ||||||
|  |       {content} | ||||||
|  |     </Box> | ||||||
|  |   ); | ||||||
|  | }; | ||||||
| @@ -35,6 +35,7 @@ export const ImageUploadCard: React.FC<ImageUploadCardProps> = ({ | |||||||
|       console.log("isDragOver"); |       console.log("isDragOver"); | ||||||
|     } |     } | ||||||
|   }, [isDragOver]); |   }, [isDragOver]); | ||||||
|  |  | ||||||
|   // --- Click to select file --- |   // --- Click to select file --- | ||||||
|   const handleZoneClick = () => { |   const handleZoneClick = () => { | ||||||
|     // Trigger the hidden file input click |     // Trigger the hidden file input click | ||||||
|   | |||||||
| @@ -1,9 +1,14 @@ | |||||||
| import { Box, Button } from "@mui/material"; | import { Box, Button } from "@mui/material"; | ||||||
| import { MediaViewer } from "@widgets"; | import { MediaViewer } from "@widgets"; | ||||||
| import { PreviewMediaDialog } from "@shared"; | import { | ||||||
|  |   PreviewMediaDialog, | ||||||
|  |   filterValidFiles, | ||||||
|  |   getAllAcceptString, | ||||||
|  | } from "@shared"; | ||||||
| import { X, Upload } from "lucide-react"; | import { X, Upload } from "lucide-react"; | ||||||
| import { observer } from "mobx-react-lite"; | import { observer } from "mobx-react-lite"; | ||||||
| import { useState, DragEvent, useRef } from "react"; | import { useState, DragEvent, useRef } from "react"; | ||||||
|  | import { toast } from "react-toastify"; | ||||||
|  |  | ||||||
| export const MediaArea = observer( | export const MediaArea = observer( | ||||||
|   ({ |   ({ | ||||||
| @@ -36,7 +41,15 @@ export const MediaArea = observer( | |||||||
|  |  | ||||||
|       const files = Array.from(e.dataTransfer.files); |       const files = Array.from(e.dataTransfer.files); | ||||||
|       if (files.length && onFilesDrop) { |       if (files.length && onFilesDrop) { | ||||||
|         onFilesDrop(files); |         const { validFiles, errors } = filterValidFiles(files); | ||||||
|  |  | ||||||
|  |         if (errors.length > 0) { | ||||||
|  |           errors.forEach((error) => toast.error(error)); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (validFiles.length > 0) { | ||||||
|  |           onFilesDrop(validFiles); | ||||||
|  |         } | ||||||
|       } |       } | ||||||
|     }; |     }; | ||||||
|  |  | ||||||
| @@ -56,7 +69,15 @@ export const MediaArea = observer( | |||||||
|     const handleFileSelect = (event: React.ChangeEvent<HTMLInputElement>) => { |     const handleFileSelect = (event: React.ChangeEvent<HTMLInputElement>) => { | ||||||
|       const files = Array.from(event.target.files || []); |       const files = Array.from(event.target.files || []); | ||||||
|       if (files.length && onFilesDrop) { |       if (files.length && onFilesDrop) { | ||||||
|         onFilesDrop(files); |         const { validFiles, errors } = filterValidFiles(files); | ||||||
|  |  | ||||||
|  |         if (errors.length > 0) { | ||||||
|  |           errors.forEach((error) => toast.error(error)); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (validFiles.length > 0) { | ||||||
|  |           onFilesDrop(validFiles); | ||||||
|  |         } | ||||||
|       } |       } | ||||||
|       // Сбрасываем значение input, чтобы можно было выбрать тот же файл снова |       // Сбрасываем значение input, чтобы можно было выбрать тот же файл снова | ||||||
|       event.target.value = ""; |       event.target.value = ""; | ||||||
| @@ -68,7 +89,7 @@ export const MediaArea = observer( | |||||||
|           type="file" |           type="file" | ||||||
|           ref={fileInputRef} |           ref={fileInputRef} | ||||||
|           onChange={handleFileSelect} |           onChange={handleFileSelect} | ||||||
|           accept="image/*,video/*,.glb,.gltf" |           accept={getAllAcceptString()} | ||||||
|           multiple |           multiple | ||||||
|           style={{ display: "none" }} |           style={{ display: "none" }} | ||||||
|         /> |         /> | ||||||
|   | |||||||
| @@ -1,8 +1,15 @@ | |||||||
| import { Box, Button } from "@mui/material"; | import { Box, Button } from "@mui/material"; | ||||||
| import { editSightStore, SelectMediaDialog, UploadMediaDialog } from "@shared"; | import { | ||||||
|  |   editSightStore, | ||||||
|  |   SelectMediaDialog, | ||||||
|  |   UploadMediaDialog, | ||||||
|  |   filterValidFiles, | ||||||
|  |   getAllAcceptString, | ||||||
|  | } from "@shared"; | ||||||
| import { Upload } from "lucide-react"; | import { Upload } from "lucide-react"; | ||||||
| import { observer } from "mobx-react-lite"; | import { observer } from "mobx-react-lite"; | ||||||
| import { useState, DragEvent, useRef } from "react"; | import { useState, DragEvent, useRef } from "react"; | ||||||
|  | import { toast } from "react-toastify"; | ||||||
|  |  | ||||||
| export const MediaAreaForSight = observer( | export const MediaAreaForSight = observer( | ||||||
|   ({ |   ({ | ||||||
| @@ -38,11 +45,18 @@ export const MediaAreaForSight = observer( | |||||||
|       setIsDragging(false); |       setIsDragging(false); | ||||||
|  |  | ||||||
|       const files = Array.from(e.dataTransfer.files); |       const files = Array.from(e.dataTransfer.files); | ||||||
|       if (files.length && onFilesDrop) { |       if (files.length) { | ||||||
|         setFileToUpload(files[0]); |         const { validFiles, errors } = filterValidFiles(files); | ||||||
|       } |  | ||||||
|  |  | ||||||
|       setUploadMediaDialogOpen(true); |         if (errors.length > 0) { | ||||||
|  |           errors.forEach((error: string) => toast.error(error)); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (validFiles.length > 0 && onFilesDrop) { | ||||||
|  |           setFileToUpload(validFiles[0]); | ||||||
|  |           setUploadMediaDialogOpen(true); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|     }; |     }; | ||||||
|  |  | ||||||
|     const handleDragOver = (e: DragEvent<HTMLDivElement>) => { |     const handleDragOver = (e: DragEvent<HTMLDivElement>) => { | ||||||
| @@ -60,10 +74,18 @@ export const MediaAreaForSight = observer( | |||||||
|  |  | ||||||
|     const handleFileSelect = (event: React.ChangeEvent<HTMLInputElement>) => { |     const handleFileSelect = (event: React.ChangeEvent<HTMLInputElement>) => { | ||||||
|       const files = Array.from(event.target.files || []); |       const files = Array.from(event.target.files || []); | ||||||
|       if (files.length && onFilesDrop) { |       if (files.length) { | ||||||
|         setFileToUpload(files[0]); |         const { validFiles, errors } = filterValidFiles(files); | ||||||
|         onFilesDrop(files); |  | ||||||
|         setUploadMediaDialogOpen(true); |         if (errors.length > 0) { | ||||||
|  |           errors.forEach((error: string) => toast.error(error)); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (validFiles.length > 0 && onFilesDrop) { | ||||||
|  |           setFileToUpload(validFiles[0]); | ||||||
|  |           onFilesDrop(validFiles); | ||||||
|  |           setUploadMediaDialogOpen(true); | ||||||
|  |         } | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       // Сбрасываем значение input, чтобы можно было выбрать тот же файл снова |       // Сбрасываем значение input, чтобы можно было выбрать тот же файл снова | ||||||
| @@ -76,7 +98,7 @@ export const MediaAreaForSight = observer( | |||||||
|           type="file" |           type="file" | ||||||
|           ref={fileInputRef} |           ref={fileInputRef} | ||||||
|           onChange={handleFileSelect} |           onChange={handleFileSelect} | ||||||
|           accept="image/*,video/*,.glb,.gltf" |           accept={getAllAcceptString()} | ||||||
|           multiple |           multiple | ||||||
|           style={{ display: "none" }} |           style={{ display: "none" }} | ||||||
|         /> |         /> | ||||||
|   | |||||||
| @@ -1,5 +1,60 @@ | |||||||
| import { Canvas } from "@react-three/fiber"; | import { Canvas } from "@react-three/fiber"; | ||||||
| import { OrbitControls, Stage, useGLTF } from "@react-three/drei"; | import { OrbitControls, Stage, useGLTF } from "@react-three/drei"; | ||||||
|  | import { useEffect, Suspense } from "react"; | ||||||
|  | import { Box, CircularProgress, Typography } from "@mui/material"; | ||||||
|  |  | ||||||
|  | // Утилита для очистки кеша GLTF | ||||||
|  | const clearGLTFCache = (url?: string) => { | ||||||
|  |   try { | ||||||
|  |     if (url) { | ||||||
|  |       // Если это blob URL, очищаем его из кеша | ||||||
|  |       if (url.startsWith("blob:")) { | ||||||
|  |         useGLTF.clear(url); | ||||||
|  |       } else { | ||||||
|  |         useGLTF.clear(url); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } catch (error) { | ||||||
|  |     console.warn("⚠️ clearGLTFCache: Ошибка при очистке кеша", error); | ||||||
|  |   } | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | // Утилита для проверки типа файла | ||||||
|  | const isValid3DFile = (url: string): boolean => { | ||||||
|  |   try { | ||||||
|  |     const urlObj = new URL(url); | ||||||
|  |     const pathname = urlObj.pathname.toLowerCase(); | ||||||
|  |     const searchParams = urlObj.searchParams; | ||||||
|  |  | ||||||
|  |     // Проверяем расширение файла в пути | ||||||
|  |     const validExtensions = [".glb", ".gltf"]; | ||||||
|  |     const hasValidExtension = validExtensions.some((ext) => | ||||||
|  |       pathname.endsWith(ext) | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |     // Проверяем параметры запроса на наличие типа файла | ||||||
|  |     const fileType = searchParams.get("type") || searchParams.get("format"); | ||||||
|  |     const hasValidType = | ||||||
|  |       fileType && ["glb", "gltf"].includes(fileType.toLowerCase()); | ||||||
|  |  | ||||||
|  |     // Если это blob URL, считаем его валидным (пользователь выбрал файл) | ||||||
|  |     const isBlobUrl = url.startsWith("blob:"); | ||||||
|  |  | ||||||
|  |     // Если это URL с токеном и нет явного расширения, считаем валидным | ||||||
|  |     // (предполагаем что сервер вернет правильный файл) | ||||||
|  |     const hasToken = searchParams.has("token"); | ||||||
|  |     const isServerUrl = hasToken && !hasValidExtension; | ||||||
|  |  | ||||||
|  |     const isValid = | ||||||
|  |       hasValidExtension || hasValidType || isBlobUrl || isServerUrl; | ||||||
|  |  | ||||||
|  |     return isValid; | ||||||
|  |   } catch (error) { | ||||||
|  |     console.warn("⚠️ isValid3DFile: Ошибка при проверке URL", error); | ||||||
|  |     // В случае ошибки парсинга URL, считаем валидным (пусть useGLTF сам разберется) | ||||||
|  |     return true; | ||||||
|  |   } | ||||||
|  | }; | ||||||
|  |  | ||||||
| type ModelViewerProps = { | type ModelViewerProps = { | ||||||
|   width?: string; |   width?: string; | ||||||
| @@ -7,21 +62,93 @@ type ModelViewerProps = { | |||||||
|   height?: string; |   height?: string; | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | const Model = ({ fileUrl }: { fileUrl: string }) => { | ||||||
|  |   // Очищаем кеш перед загрузкой новой модели | ||||||
|  |   useEffect(() => { | ||||||
|  |     // Очищаем кеш для текущего URL | ||||||
|  |     clearGLTFCache(fileUrl); | ||||||
|  |   }, [fileUrl]); | ||||||
|  |  | ||||||
|  |   // Проверяем валидность файла перед загрузкой (только для blob URL) | ||||||
|  |   if (fileUrl.startsWith("blob:") && !isValid3DFile(fileUrl)) { | ||||||
|  |     console.warn("⚠️ Model: Попытка загрузить невалидный 3D файл", { fileUrl }); | ||||||
|  |     throw new Error(`Файл не является корректной 3D моделью: ${fileUrl}`); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   const { scene } = useGLTF(fileUrl); | ||||||
|  |   return <primitive object={scene} />; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const LoadingFallback = () => { | ||||||
|  |   return ( | ||||||
|  |     <Box | ||||||
|  |       sx={{ | ||||||
|  |         position: "absolute", | ||||||
|  |         top: "50%", | ||||||
|  |         left: "50%", | ||||||
|  |         transform: "translate(-50%, -50%)", | ||||||
|  |         display: "flex", | ||||||
|  |         flexDirection: "column", | ||||||
|  |         alignItems: "center", | ||||||
|  |         gap: 2, | ||||||
|  |         zIndex: 1000, | ||||||
|  |         backgroundColor: "background.paper", | ||||||
|  |         p: 3, | ||||||
|  |         borderRadius: 2, | ||||||
|  |       }} | ||||||
|  |     > | ||||||
|  |       <CircularProgress size={48} /> | ||||||
|  |       <Typography | ||||||
|  |         variant="body2" | ||||||
|  |         color="text.secondary" | ||||||
|  |         style={{ whiteSpace: "nowrap" }} | ||||||
|  |       > | ||||||
|  |         Загрузка 3D модели... | ||||||
|  |       </Typography> | ||||||
|  |     </Box> | ||||||
|  |   ); | ||||||
|  | }; | ||||||
|  |  | ||||||
| export const ThreeView = ({ | export const ThreeView = ({ | ||||||
|   fileUrl, |   fileUrl, | ||||||
|   height = "100%", |   height = "100%", | ||||||
|   width = "100%", |   width = "100%", | ||||||
| }: ModelViewerProps) => { | }: ModelViewerProps) => { | ||||||
|   const { scene } = useGLTF(fileUrl); |   // Проверяем валидность файла (только для blob URL) | ||||||
|  |   useEffect(() => { | ||||||
|  |     if (fileUrl.startsWith("blob:") && !isValid3DFile(fileUrl)) { | ||||||
|  |       console.warn("⚠️ ThreeView: Невалидный 3D файл", { fileUrl }); | ||||||
|  |     } | ||||||
|  |   }, [fileUrl]); | ||||||
|  |  | ||||||
|  |   // Очищаем кеш при размонтировании и при смене URL | ||||||
|  |   useEffect(() => { | ||||||
|  |     // Очищаем кеш сразу при монтировании компонента | ||||||
|  |     clearGLTFCache(fileUrl); | ||||||
|  |  | ||||||
|  |     return () => { | ||||||
|  |       clearGLTFCache(fileUrl); | ||||||
|  |     }; | ||||||
|  |   }, [fileUrl]); | ||||||
|  |  | ||||||
|   return ( |   return ( | ||||||
|     <Canvas style={{ height: height, width: width }}> |     <Box sx={{ position: "relative", width, height }}> | ||||||
|       <ambientLight /> |       <Suspense fallback={<LoadingFallback />}> | ||||||
|       <directionalLight /> |         <Canvas | ||||||
|       <Stage environment="city" intensity={0.6}> |           style={{ height: height, width: width }} | ||||||
|         <primitive object={scene} /> |           camera={{ | ||||||
|       </Stage> |             position: [1, 1, 1], | ||||||
|       <OrbitControls /> |             fov: 30, | ||||||
|     </Canvas> |           }} | ||||||
|  |         > | ||||||
|  |           <ambientLight /> | ||||||
|  |           <directionalLight /> | ||||||
|  |           <Stage environment="city" intensity={0.6} adjustCamera={false}> | ||||||
|  |             <Model fileUrl={fileUrl} /> | ||||||
|  |           </Stage> | ||||||
|  |           <OrbitControls /> | ||||||
|  |         </Canvas> | ||||||
|  |       </Suspense> | ||||||
|  |     </Box> | ||||||
|   ); |   ); | ||||||
| }; | }; | ||||||
|   | |||||||
							
								
								
									
										240
									
								
								src/widgets/MediaViewer/ThreeViewErrorBoundary.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										240
									
								
								src/widgets/MediaViewer/ThreeViewErrorBoundary.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,240 @@ | |||||||
|  | import React, { Component, ReactNode } from "react"; | ||||||
|  | import { Box, Button, Typography, Paper } from "@mui/material"; | ||||||
|  | import { RefreshCw, AlertTriangle } from "lucide-react"; | ||||||
|  |  | ||||||
|  | interface Props { | ||||||
|  |   children: ReactNode; | ||||||
|  |   onReset?: () => void; | ||||||
|  |   resetKey?: number | string; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | interface State { | ||||||
|  |   hasError: boolean; | ||||||
|  |   error: Error | null; | ||||||
|  |   lastResetKey?: number | string; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export class ThreeViewErrorBoundary extends Component<Props, State> { | ||||||
|  |   constructor(props: Props) { | ||||||
|  |     super(props); | ||||||
|  |     this.state = { | ||||||
|  |       hasError: false, | ||||||
|  |       error: null, | ||||||
|  |       lastResetKey: props.resetKey, | ||||||
|  |     }; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   static getDerivedStateFromError(error: Error): Partial<State> { | ||||||
|  |     return { | ||||||
|  |       hasError: true, | ||||||
|  |       error, | ||||||
|  |     }; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   static getDerivedStateFromProps( | ||||||
|  |     props: Props, | ||||||
|  |     state: State | ||||||
|  |   ): Partial<State> | null { | ||||||
|  |     // Сбрасываем ошибку ТОЛЬКО при смене медиа (когда меняется ID в resetKey) | ||||||
|  |     if ( | ||||||
|  |       props.resetKey !== state.lastResetKey && | ||||||
|  |       state.lastResetKey !== undefined | ||||||
|  |     ) { | ||||||
|  |       const oldMediaId = String(state.lastResetKey).split("-")[0]; | ||||||
|  |       const newMediaId = String(props.resetKey).split("-")[0]; | ||||||
|  |  | ||||||
|  |       // Сбрасываем ошибку только если изменился ID медиа (пользователь выбрал другую модель) | ||||||
|  |       if (oldMediaId !== newMediaId) { | ||||||
|  |         return { | ||||||
|  |           hasError: false, | ||||||
|  |           error: null, | ||||||
|  |           lastResetKey: props.resetKey, | ||||||
|  |         }; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       // Если изменился только счетчик (нажата кнопка "Попробовать снова"), обновляем lastResetKey | ||||||
|  |       // но не сбрасываем ошибку автоматически - ждем результата загрузки | ||||||
|  |  | ||||||
|  |       return { | ||||||
|  |         lastResetKey: props.resetKey, | ||||||
|  |       }; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (state.lastResetKey === undefined && props.resetKey !== undefined) { | ||||||
|  |       return { | ||||||
|  |         lastResetKey: props.resetKey, | ||||||
|  |       }; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return null; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   componentDidCatch(error: Error, errorInfo: React.ErrorInfo) { | ||||||
|  |     console.error("❌ ThreeViewErrorBoundary: Ошибка загрузки 3D модели", { | ||||||
|  |       error: error.message, | ||||||
|  |       stack: error.stack, | ||||||
|  |       componentStack: errorInfo.componentStack, | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   getErrorMessage = () => { | ||||||
|  |     const errorMessage = this.state.error?.message || ""; | ||||||
|  |  | ||||||
|  |     if ( | ||||||
|  |       errorMessage.includes("not valid JSON") || | ||||||
|  |       errorMessage.includes("Unexpected token") | ||||||
|  |     ) { | ||||||
|  |       return "Неверный формат файла. Убедитесь, что файл является корректной 3D моделью в формате GLB/GLTF."; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (errorMessage.includes("Could not load")) { | ||||||
|  |       return "Не удалось загрузить файл 3D модели. Проверьте, что файл существует и доступен."; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (errorMessage.includes("404") || errorMessage.includes("Not Found")) { | ||||||
|  |       return "Файл 3D модели не найден на сервере."; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (errorMessage.includes("Network") || errorMessage.includes("fetch")) { | ||||||
|  |       return "Ошибка сети при загрузке 3D модели. Проверьте интернет-соединение."; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return ( | ||||||
|  |       errorMessage || "Произошла неизвестная ошибка при загрузке 3D модели" | ||||||
|  |     ); | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   getErrorReasons = () => { | ||||||
|  |     const errorMessage = this.state.error?.message || ""; | ||||||
|  |  | ||||||
|  |     if ( | ||||||
|  |       errorMessage.includes("not valid JSON") || | ||||||
|  |       errorMessage.includes("Unexpected token") | ||||||
|  |     ) { | ||||||
|  |       return [ | ||||||
|  |         "Файл не является 3D моделью", | ||||||
|  |         "Загружен файл неподдерживаемого формата", | ||||||
|  |         "Файл поврежден или не полностью загружен", | ||||||
|  |         "Используйте только GLB или GLTF форматы", | ||||||
|  |       ]; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return [ | ||||||
|  |       "Поврежденный файл модели", | ||||||
|  |       "Неподдерживаемый формат", | ||||||
|  |       "Проблемы с загрузкой файла", | ||||||
|  |     ]; | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   handleReset = () => { | ||||||
|  |     // Сначала сбрасываем состояние ошибки | ||||||
|  |     this.setState( | ||||||
|  |       { | ||||||
|  |         hasError: false, | ||||||
|  |         error: null, | ||||||
|  |       }, | ||||||
|  |       () => { | ||||||
|  |         // После того как состояние обновилось, вызываем callback для изменения resetKey | ||||||
|  |         // Это приведет к пересозданию компонента и новой попытке загрузки | ||||||
|  |         this.props.onReset?.(); | ||||||
|  |       } | ||||||
|  |     ); | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   handleClose = () => { | ||||||
|  |     this.setState({ | ||||||
|  |       hasError: false, | ||||||
|  |       error: null, | ||||||
|  |     }); | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   render() { | ||||||
|  |     if (this.state.hasError) { | ||||||
|  |       return ( | ||||||
|  |         <Box | ||||||
|  |           sx={{ | ||||||
|  |             width: "100%", | ||||||
|  |             height: "100%", | ||||||
|  |             display: "flex", | ||||||
|  |             justifyContent: "center", | ||||||
|  |             alignItems: "center", | ||||||
|  |             p: 2, | ||||||
|  |           }} | ||||||
|  |         > | ||||||
|  |           <Paper | ||||||
|  |             elevation={3} | ||||||
|  |             sx={{ | ||||||
|  |               p: 3, | ||||||
|  |               maxWidth: 500, | ||||||
|  |               width: "100%", | ||||||
|  |               position: "relative", | ||||||
|  |               backgroundColor: "error.light", | ||||||
|  |               color: "error.contrastText", | ||||||
|  |             }} | ||||||
|  |           > | ||||||
|  |             <Box sx={{ display: "flex", alignItems: "center", mb: 2 }}> | ||||||
|  |               <AlertTriangle size={32} style={{ marginRight: 12 }} /> | ||||||
|  |               <Typography variant="h6" component="h2"> | ||||||
|  |                 Ошибка загрузки 3D модели | ||||||
|  |               </Typography> | ||||||
|  |             </Box> | ||||||
|  |  | ||||||
|  |             <Typography variant="body2" sx={{ mb: 2 }}> | ||||||
|  |               {this.getErrorMessage()} | ||||||
|  |             </Typography> | ||||||
|  |  | ||||||
|  |             <Typography variant="caption" sx={{ mb: 2, display: "block" }}> | ||||||
|  |               Возможные причины: | ||||||
|  |               <ul style={{ margin: "8px 0", paddingLeft: "20px" }}> | ||||||
|  |                 {this.getErrorReasons().map((reason, index) => ( | ||||||
|  |                   <li key={index}>{reason}</li> | ||||||
|  |                 ))} | ||||||
|  |               </ul> | ||||||
|  |             </Typography> | ||||||
|  |  | ||||||
|  |             {this.state.error?.message && ( | ||||||
|  |               <Typography | ||||||
|  |                 variant="caption" | ||||||
|  |                 sx={{ | ||||||
|  |                   mb: 2, | ||||||
|  |                   display: "block", | ||||||
|  |                   fontFamily: "monospace", | ||||||
|  |                   backgroundColor: "rgba(0, 0, 0, 0.1)", | ||||||
|  |                   p: 1, | ||||||
|  |                   borderRadius: 1, | ||||||
|  |                   fontSize: "0.7rem", | ||||||
|  |                   wordBreak: "break-word", | ||||||
|  |                   maxHeight: "100px", | ||||||
|  |                   overflow: "auto", | ||||||
|  |                 }} | ||||||
|  |               > | ||||||
|  |                 {this.state.error.message} | ||||||
|  |               </Typography> | ||||||
|  |             )} | ||||||
|  |  | ||||||
|  |             <Box sx={{ display: "flex", gap: 1 }}> | ||||||
|  |               <Button | ||||||
|  |                 variant="contained" | ||||||
|  |                 startIcon={<RefreshCw size={16} />} | ||||||
|  |                 onClick={() => { | ||||||
|  |                   this.handleReset(); | ||||||
|  |                 }} | ||||||
|  |                 sx={{ | ||||||
|  |                   backgroundColor: "error.contrastText", | ||||||
|  |                   color: "error.main", | ||||||
|  |                   "&:hover": { | ||||||
|  |                     backgroundColor: "rgba(255, 255, 255, 0.9)", | ||||||
|  |                   }, | ||||||
|  |                 }} | ||||||
|  |               > | ||||||
|  |                 Попробовать снова | ||||||
|  |               </Button> | ||||||
|  |             </Box> | ||||||
|  |           </Paper> | ||||||
|  |         </Box> | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return this.props.children; | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -1,7 +1,10 @@ | |||||||
| import { Box } from "@mui/material"; | import { Box } from "@mui/material"; | ||||||
|  | import { useState, useEffect } from "react"; | ||||||
|  |  | ||||||
| import { ReactPhotoSphereViewer } from "react-photo-sphere-viewer"; | import { ReactPhotoSphereViewer } from "react-photo-sphere-viewer"; | ||||||
| import { ThreeView } from "./ThreeView"; | import { ThreeView } from "./ThreeView"; | ||||||
|  | import { ThreeViewErrorBoundary } from "./ThreeViewErrorBoundary"; | ||||||
|  | import { clearMediaTransitionCache } from "../../shared/lib/gltfCacheManager"; | ||||||
|  |  | ||||||
| export interface MediaData { | export interface MediaData { | ||||||
|   id: string | number; |   id: string | number; | ||||||
| @@ -25,6 +28,33 @@ export function MediaViewer({ | |||||||
|   fullHeight?: boolean; |   fullHeight?: boolean; | ||||||
| }>) { | }>) { | ||||||
|   const token = localStorage.getItem("token"); |   const token = localStorage.getItem("token"); | ||||||
|  |   const [resetKey, setResetKey] = useState(0); | ||||||
|  |   const [previousMediaId, setPreviousMediaId] = useState< | ||||||
|  |     string | number | null | ||||||
|  |   >(null); | ||||||
|  |  | ||||||
|  |   useEffect(() => { | ||||||
|  |     if (media?.id !== previousMediaId) { | ||||||
|  |       // Используем новый cache manager для очистки кеша | ||||||
|  |       clearMediaTransitionCache( | ||||||
|  |         previousMediaId, | ||||||
|  |         media?.id || null, | ||||||
|  |         media?.media_type | ||||||
|  |       ); | ||||||
|  |  | ||||||
|  |       setResetKey(0); | ||||||
|  |       setPreviousMediaId(media?.id || null); | ||||||
|  |     } | ||||||
|  |   }, [media?.id, media?.media_type, previousMediaId]); | ||||||
|  |  | ||||||
|  |   const handleReset = () => { | ||||||
|  |     setResetKey((prev) => { | ||||||
|  |       const newKey = prev + 1; | ||||||
|  |  | ||||||
|  |       return newKey; | ||||||
|  |     }); | ||||||
|  |   }; | ||||||
|  |  | ||||||
|   return ( |   return ( | ||||||
|     <Box |     <Box | ||||||
|       className={className} |       className={className} | ||||||
| @@ -48,11 +78,6 @@ export function MediaViewer({ | |||||||
|           style={{ |           style={{ | ||||||
|             height: fullHeight ? "100%" : height ? height : "auto", |             height: fullHeight ? "100%" : height ? height : "auto", | ||||||
|             width: fullWidth ? "100%" : width ? width : "auto", |             width: fullWidth ? "100%" : width ? width : "auto", | ||||||
|             ...(media?.filename?.toLowerCase().endsWith(".webp") && { |  | ||||||
|               maxWidth: "300px", |  | ||||||
|               maxHeight: "300px", |  | ||||||
|               objectFit: "contain", |  | ||||||
|             }), |  | ||||||
|           }} |           }} | ||||||
|         /> |         /> | ||||||
|       )} |       )} | ||||||
| @@ -108,13 +133,19 @@ export function MediaViewer({ | |||||||
|       )} |       )} | ||||||
|  |  | ||||||
|       {media?.media_type === 6 && ( |       {media?.media_type === 6 && ( | ||||||
|         <ThreeView |         <ThreeViewErrorBoundary | ||||||
|           fileUrl={`${import.meta.env.VITE_KRBL_MEDIA}${ |           onReset={handleReset} | ||||||
|             media?.id |           resetKey={`${media?.id}-${resetKey}`} | ||||||
|           }/download?token=${token}`} |         > | ||||||
|           height={height ? height : "500px"} |           <ThreeView | ||||||
|           width={width ? width : "500px"} |             key={`3d-model-${media?.id}-${resetKey}`} | ||||||
|         /> |             fileUrl={`${import.meta.env.VITE_KRBL_MEDIA}${ | ||||||
|  |               media?.id | ||||||
|  |             }/download?token=${token}`} | ||||||
|  |             height={height ? height : "500px"} | ||||||
|  |             width={width ? width : "500px"} | ||||||
|  |           /> | ||||||
|  |         </ThreeViewErrorBoundary> | ||||||
|       )} |       )} | ||||||
|     </Box> |     </Box> | ||||||
|   ); |   ); | ||||||
|   | |||||||
| @@ -316,31 +316,35 @@ export const LeftWidgetTab = observer( | |||||||
|                             }} |                             }} | ||||||
|                             fullWidth |                             fullWidth | ||||||
|                           /> |                           /> | ||||||
|                           <img |                           {sight.common.watermark_lu && ( | ||||||
|                             src={`${import.meta.env.VITE_KRBL_MEDIA}${ |                             <img | ||||||
|                               sight.common.watermark_lu |                               src={`${import.meta.env.VITE_KRBL_MEDIA}${ | ||||||
|                             }/download?token=${token}`} |                                 sight.common.watermark_lu | ||||||
|                             alt="preview" |                               }/download?token=${token}`} | ||||||
|                             className="absolute top-4 left-4 z-10" |                               alt="preview" | ||||||
|                             style={{ |                               className="absolute top-4 left-4 z-10" | ||||||
|                               width: "30px", |                               style={{ | ||||||
|                               height: "30px", |                                 width: "30px", | ||||||
|                               objectFit: "contain", |                                 height: "30px", | ||||||
|                             }} |                                 objectFit: "contain", | ||||||
|                           /> |                               }} | ||||||
|  |                             /> | ||||||
|  |                           )} | ||||||
|  |  | ||||||
|                           <img |                           {sight.common.watermark_rd && ( | ||||||
|                             src={`${import.meta.env.VITE_KRBL_MEDIA}${ |                             <img | ||||||
|                               sight.common.watermark_rd |                               src={`${import.meta.env.VITE_KRBL_MEDIA}${ | ||||||
|                             }/download?token=${token}`} |                                 sight.common.watermark_rd | ||||||
|                             alt="preview" |                               }/download?token=${token}`} | ||||||
|                             className="absolute bottom-4 right-4 z-10" |                               alt="preview" | ||||||
|                             style={{ |                               className="absolute bottom-4 right-4 z-10" | ||||||
|                               width: "30px", |                               style={{ | ||||||
|                               height: "30px", |                                 width: "30px", | ||||||
|                               objectFit: "contain", |                                 height: "30px", | ||||||
|                             }} |                                 objectFit: "contain", | ||||||
|                           /> |                               }} | ||||||
|  |                             /> | ||||||
|  |                           )} | ||||||
|                         </> |                         </> | ||||||
|                       ) : ( |                       ) : ( | ||||||
|                         <ImagePlus size={48} color="white" /> |                         <ImagePlus size={48} color="white" /> | ||||||
|   | |||||||
| @@ -87,7 +87,6 @@ export const RightWidgetTab = observer( | |||||||
|         } |         } | ||||||
|       }; |       }; | ||||||
|       fetchData(); |       fetchData(); | ||||||
|       console.log(sight[language].right); |  | ||||||
|     }, [sight.common.id]); |     }, [sight.common.id]); | ||||||
|  |  | ||||||
|     const [activeArticleIndex, setActiveArticleIndex] = useState<number | null>( |     const [activeArticleIndex, setActiveArticleIndex] = useState<number | null>( | ||||||
| @@ -175,10 +174,6 @@ export const RightWidgetTab = observer( | |||||||
|       toast.success("Достопримечательность сохранена"); |       toast.success("Достопримечательность сохранена"); | ||||||
|     }; |     }; | ||||||
|  |  | ||||||
|     useEffect(() => { |  | ||||||
|       console.log(sight[language].right); |  | ||||||
|     }, [sight[language].right]); |  | ||||||
|  |  | ||||||
|     const handleDragEnd = (result: DropResult) => { |     const handleDragEnd = (result: DropResult) => { | ||||||
|       const { source, destination } = result; |       const { source, destination } = result; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -43,8 +43,6 @@ export const EditStationModal = observer( | |||||||
|     } = routeStore; |     } = routeStore; | ||||||
|  |  | ||||||
|     const handleSave = async () => { |     const handleSave = async () => { | ||||||
|       console.log(routeId, selectedStationId); |  | ||||||
|  |  | ||||||
|       await saveRouteStations(Number(routeId), selectedStationId); |       await saveRouteStations(Number(routeId), selectedStationId); | ||||||
|       toast.success("Успешно сохранено"); |       toast.success("Успешно сохранено"); | ||||||
|       onClose(); |       onClose(); | ||||||
|   | |||||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										441
									
								
								yarn.lock
									
									
									
									
									
								
							
							
						
						
									
										441
									
								
								yarn.lock
									
									
									
									
									
								
							| @@ -24,7 +24,7 @@ | |||||||
|   resolved "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.27.3.tgz" |   resolved "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.27.3.tgz" | ||||||
|   integrity sha512-V42wFfx1ymFte+ecf6iXghnnP8kWTO+ZLXIyZq+1LAXHHvTZdVxicn4yiVYdYMGaCO3tmqub11AorKkv+iodqw== |   integrity sha512-V42wFfx1ymFte+ecf6iXghnnP8kWTO+ZLXIyZq+1LAXHHvTZdVxicn4yiVYdYMGaCO3tmqub11AorKkv+iodqw== | ||||||
|  |  | ||||||
| "@babel/core@^7.0.0", "@babel/core@^7.0.0-0", "@babel/core@^7.26.10": | "@babel/core@^7.26.10": | ||||||
|   version "7.27.3" |   version "7.27.3" | ||||||
|   resolved "https://registry.npmjs.org/@babel/core/-/core-7.27.3.tgz" |   resolved "https://registry.npmjs.org/@babel/core/-/core-7.27.3.tgz" | ||||||
|   integrity sha512-hyrN8ivxfvJ4i0fIJuV4EOlV0WDMz5Ui4StRTgVaAvWeiRCilXgwVvxJKtFQ3TKtHgJscB2YiXKGNJuVwhQMtA== |   integrity sha512-hyrN8ivxfvJ4i0fIJuV4EOlV0WDMz5Ui4StRTgVaAvWeiRCilXgwVvxJKtFQ3TKtHgJscB2YiXKGNJuVwhQMtA== | ||||||
| @@ -173,6 +173,28 @@ | |||||||
|   resolved "https://registry.npmjs.org/@dimforge/rapier3d-compat/-/rapier3d-compat-0.12.0.tgz" |   resolved "https://registry.npmjs.org/@dimforge/rapier3d-compat/-/rapier3d-compat-0.12.0.tgz" | ||||||
|   integrity sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow== |   integrity sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow== | ||||||
|  |  | ||||||
|  | "@emnapi/core@^1.4.3": | ||||||
|  |   version "1.6.0" | ||||||
|  |   resolved "https://registry.yarnpkg.com/@emnapi/core/-/core-1.6.0.tgz#517f65d1c8270d5d5aa1aad660d5acb897430dca" | ||||||
|  |   integrity sha512-zq/ay+9fNIJJtJiZxdTnXS20PllcYMX3OE23ESc4HK/bdYu3cOWYVhsOhVnXALfU/uqJIxn5NBPd9z4v+SfoSg== | ||||||
|  |   dependencies: | ||||||
|  |     "@emnapi/wasi-threads" "1.1.0" | ||||||
|  |     tslib "^2.4.0" | ||||||
|  |  | ||||||
|  | "@emnapi/runtime@^1.4.3": | ||||||
|  |   version "1.6.0" | ||||||
|  |   resolved "https://registry.yarnpkg.com/@emnapi/runtime/-/runtime-1.6.0.tgz#8fe297e0090f6e89a57a1f31f1c440bdbc3c01d8" | ||||||
|  |   integrity sha512-obtUmAHTMjll499P+D9A3axeJFlhdjOWdKUNs/U6QIGT7V5RjcUW1xToAzjvmgTSQhDbYn/NwfTRoJcQ2rNBxA== | ||||||
|  |   dependencies: | ||||||
|  |     tslib "^2.4.0" | ||||||
|  |  | ||||||
|  | "@emnapi/wasi-threads@1.1.0", "@emnapi/wasi-threads@^1.0.2": | ||||||
|  |   version "1.1.0" | ||||||
|  |   resolved "https://registry.yarnpkg.com/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz#60b2102fddc9ccb78607e4a3cf8403ea69be41bf" | ||||||
|  |   integrity sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ== | ||||||
|  |   dependencies: | ||||||
|  |     tslib "^2.4.0" | ||||||
|  |  | ||||||
| "@emotion/babel-plugin@^11.13.5": | "@emotion/babel-plugin@^11.13.5": | ||||||
|   version "11.13.5" |   version "11.13.5" | ||||||
|   resolved "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz" |   resolved "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz" | ||||||
| @@ -218,7 +240,7 @@ | |||||||
|   resolved "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz" |   resolved "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz" | ||||||
|   integrity sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ== |   integrity sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ== | ||||||
|  |  | ||||||
| "@emotion/react@^11.0.0-rc.0", "@emotion/react@^11.14.0", "@emotion/react@^11.4.1", "@emotion/react@^11.5.0", "@emotion/react@^11.9.0": | "@emotion/react@^11.14.0": | ||||||
|   version "11.14.0" |   version "11.14.0" | ||||||
|   resolved "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz" |   resolved "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz" | ||||||
|   integrity sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA== |   integrity sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA== | ||||||
| @@ -248,7 +270,7 @@ | |||||||
|   resolved "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz" |   resolved "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz" | ||||||
|   integrity sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg== |   integrity sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg== | ||||||
|  |  | ||||||
| "@emotion/styled@^11.14.0", "@emotion/styled@^11.3.0", "@emotion/styled@^11.8.1": | "@emotion/styled@^11.14.0": | ||||||
|   version "11.14.0" |   version "11.14.0" | ||||||
|   resolved "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.0.tgz" |   resolved "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.0.tgz" | ||||||
|   integrity sha512-XxfOnXFffatap2IyCeJyNov3kiDQWoR08gPUQxvbL7fxKryGBKUZUkG6Hz48DZwVrJSVh9sJboyV1Ds4OW6SgA== |   integrity sha512-XxfOnXFffatap2IyCeJyNov3kiDQWoR08gPUQxvbL7fxKryGBKUZUkG6Hz48DZwVrJSVh9sJboyV1Ds4OW6SgA== | ||||||
| @@ -280,11 +302,131 @@ | |||||||
|   resolved "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz" |   resolved "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz" | ||||||
|   integrity sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg== |   integrity sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg== | ||||||
|  |  | ||||||
|  | "@esbuild/aix-ppc64@0.25.5": | ||||||
|  |   version "0.25.5" | ||||||
|  |   resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.5.tgz#4e0f91776c2b340e75558f60552195f6fad09f18" | ||||||
|  |   integrity sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA== | ||||||
|  |  | ||||||
|  | "@esbuild/android-arm64@0.25.5": | ||||||
|  |   version "0.25.5" | ||||||
|  |   resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.25.5.tgz#bc766407f1718923f6b8079c8c61bf86ac3a6a4f" | ||||||
|  |   integrity sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg== | ||||||
|  |  | ||||||
|  | "@esbuild/android-arm@0.25.5": | ||||||
|  |   version "0.25.5" | ||||||
|  |   resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.25.5.tgz#4290d6d3407bae3883ad2cded1081a234473ce26" | ||||||
|  |   integrity sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA== | ||||||
|  |  | ||||||
|  | "@esbuild/android-x64@0.25.5": | ||||||
|  |   version "0.25.5" | ||||||
|  |   resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.25.5.tgz#40c11d9cbca4f2406548c8a9895d321bc3b35eff" | ||||||
|  |   integrity sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw== | ||||||
|  |  | ||||||
| "@esbuild/darwin-arm64@0.25.5": | "@esbuild/darwin-arm64@0.25.5": | ||||||
|   version "0.25.5" |   version "0.25.5" | ||||||
|   resolved "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.5.tgz" |   resolved "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.5.tgz" | ||||||
|   integrity sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ== |   integrity sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ== | ||||||
|  |  | ||||||
|  | "@esbuild/darwin-x64@0.25.5": | ||||||
|  |   version "0.25.5" | ||||||
|  |   resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.25.5.tgz#e27a5d92a14886ef1d492fd50fc61a2d4d87e418" | ||||||
|  |   integrity sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ== | ||||||
|  |  | ||||||
|  | "@esbuild/freebsd-arm64@0.25.5": | ||||||
|  |   version "0.25.5" | ||||||
|  |   resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.5.tgz#97cede59d638840ca104e605cdb9f1b118ba0b1c" | ||||||
|  |   integrity sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw== | ||||||
|  |  | ||||||
|  | "@esbuild/freebsd-x64@0.25.5": | ||||||
|  |   version "0.25.5" | ||||||
|  |   resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.25.5.tgz#71c77812042a1a8190c3d581e140d15b876b9c6f" | ||||||
|  |   integrity sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw== | ||||||
|  |  | ||||||
|  | "@esbuild/linux-arm64@0.25.5": | ||||||
|  |   version "0.25.5" | ||||||
|  |   resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.25.5.tgz#f7b7c8f97eff8ffd2e47f6c67eb5c9765f2181b8" | ||||||
|  |   integrity sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg== | ||||||
|  |  | ||||||
|  | "@esbuild/linux-arm@0.25.5": | ||||||
|  |   version "0.25.5" | ||||||
|  |   resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.25.5.tgz#2a0be71b6cd8201fa559aea45598dffabc05d911" | ||||||
|  |   integrity sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw== | ||||||
|  |  | ||||||
|  | "@esbuild/linux-ia32@0.25.5": | ||||||
|  |   version "0.25.5" | ||||||
|  |   resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.25.5.tgz#763414463cd9ea6fa1f96555d2762f9f84c61783" | ||||||
|  |   integrity sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA== | ||||||
|  |  | ||||||
|  | "@esbuild/linux-loong64@0.25.5": | ||||||
|  |   version "0.25.5" | ||||||
|  |   resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.25.5.tgz#428cf2213ff786a502a52c96cf29d1fcf1eb8506" | ||||||
|  |   integrity sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg== | ||||||
|  |  | ||||||
|  | "@esbuild/linux-mips64el@0.25.5": | ||||||
|  |   version "0.25.5" | ||||||
|  |   resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.25.5.tgz#5cbcc7fd841b4cd53358afd33527cd394e325d96" | ||||||
|  |   integrity sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg== | ||||||
|  |  | ||||||
|  | "@esbuild/linux-ppc64@0.25.5": | ||||||
|  |   version "0.25.5" | ||||||
|  |   resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.25.5.tgz#0d954ab39ce4f5e50f00c4f8c4fd38f976c13ad9" | ||||||
|  |   integrity sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ== | ||||||
|  |  | ||||||
|  | "@esbuild/linux-riscv64@0.25.5": | ||||||
|  |   version "0.25.5" | ||||||
|  |   resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.25.5.tgz#0e7dd30730505abd8088321e8497e94b547bfb1e" | ||||||
|  |   integrity sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA== | ||||||
|  |  | ||||||
|  | "@esbuild/linux-s390x@0.25.5": | ||||||
|  |   version "0.25.5" | ||||||
|  |   resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.25.5.tgz#5669af81327a398a336d7e40e320b5bbd6e6e72d" | ||||||
|  |   integrity sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ== | ||||||
|  |  | ||||||
|  | "@esbuild/linux-x64@0.25.5": | ||||||
|  |   version "0.25.5" | ||||||
|  |   resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.25.5.tgz#b2357dd153aa49038967ddc1ffd90c68a9d2a0d4" | ||||||
|  |   integrity sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw== | ||||||
|  |  | ||||||
|  | "@esbuild/netbsd-arm64@0.25.5": | ||||||
|  |   version "0.25.5" | ||||||
|  |   resolved "https://registry.yarnpkg.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.5.tgz#53b4dfb8fe1cee93777c9e366893bd3daa6ba63d" | ||||||
|  |   integrity sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw== | ||||||
|  |  | ||||||
|  | "@esbuild/netbsd-x64@0.25.5": | ||||||
|  |   version "0.25.5" | ||||||
|  |   resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.25.5.tgz#a0206f6314ce7dc8713b7732703d0f58de1d1e79" | ||||||
|  |   integrity sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ== | ||||||
|  |  | ||||||
|  | "@esbuild/openbsd-arm64@0.25.5": | ||||||
|  |   version "0.25.5" | ||||||
|  |   resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.5.tgz#2a796c87c44e8de78001d808c77d948a21ec22fd" | ||||||
|  |   integrity sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw== | ||||||
|  |  | ||||||
|  | "@esbuild/openbsd-x64@0.25.5": | ||||||
|  |   version "0.25.5" | ||||||
|  |   resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.25.5.tgz#28d0cd8909b7fa3953af998f2b2ed34f576728f0" | ||||||
|  |   integrity sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg== | ||||||
|  |  | ||||||
|  | "@esbuild/sunos-x64@0.25.5": | ||||||
|  |   version "0.25.5" | ||||||
|  |   resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.25.5.tgz#a28164f5b997e8247d407e36c90d3fd5ddbe0dc5" | ||||||
|  |   integrity sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA== | ||||||
|  |  | ||||||
|  | "@esbuild/win32-arm64@0.25.5": | ||||||
|  |   version "0.25.5" | ||||||
|  |   resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.25.5.tgz#6eadbead38e8bd12f633a5190e45eff80e24007e" | ||||||
|  |   integrity sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw== | ||||||
|  |  | ||||||
|  | "@esbuild/win32-ia32@0.25.5": | ||||||
|  |   version "0.25.5" | ||||||
|  |   resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.25.5.tgz#bab6288005482f9ed2adb9ded7e88eba9a62cc0d" | ||||||
|  |   integrity sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ== | ||||||
|  |  | ||||||
|  | "@esbuild/win32-x64@0.25.5": | ||||||
|  |   version "0.25.5" | ||||||
|  |   resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.25.5.tgz#7fc114af5f6563f19f73324b5d5ff36ece0803d1" | ||||||
|  |   integrity sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g== | ||||||
|  |  | ||||||
| "@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.7.0": | "@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.7.0": | ||||||
|   version "4.7.0" |   version "4.7.0" | ||||||
|   resolved "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz" |   resolved "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz" | ||||||
| @@ -333,7 +475,7 @@ | |||||||
|     minimatch "^3.1.2" |     minimatch "^3.1.2" | ||||||
|     strip-json-comments "^3.1.1" |     strip-json-comments "^3.1.1" | ||||||
|  |  | ||||||
| "@eslint/js@^9.25.0", "@eslint/js@9.27.0": | "@eslint/js@9.27.0", "@eslint/js@^9.25.0": | ||||||
|   version "9.27.0" |   version "9.27.0" | ||||||
|   resolved "https://registry.npmjs.org/@eslint/js/-/js-9.27.0.tgz" |   resolved "https://registry.npmjs.org/@eslint/js/-/js-9.27.0.tgz" | ||||||
|   integrity sha512-G5JD9Tu5HJEu4z2Uo4aHY2sLV64B7CDMXxFzqzjl3NKd6RVzSXNoE80jk7Y0lJkTTkjiIhBAqmlYwjuBY3tvpA== |   integrity sha512-G5JD9Tu5HJEu4z2Uo4aHY2sLV64B7CDMXxFzqzjl3NKd6RVzSXNoE80jk7Y0lJkTTkjiIhBAqmlYwjuBY3tvpA== | ||||||
| @@ -453,7 +595,7 @@ | |||||||
|   dependencies: |   dependencies: | ||||||
|     "@babel/runtime" "^7.27.1" |     "@babel/runtime" "^7.27.1" | ||||||
|  |  | ||||||
| "@mui/material@^5.15.14 || ^6.0.0 || ^7.0.0", "@mui/material@^7.1.0", "@mui/material@^7.1.1": | "@mui/material@^7.1.0": | ||||||
|   version "7.1.1" |   version "7.1.1" | ||||||
|   resolved "https://registry.npmjs.org/@mui/material/-/material-7.1.1.tgz" |   resolved "https://registry.npmjs.org/@mui/material/-/material-7.1.1.tgz" | ||||||
|   integrity sha512-mTpdmdZCaHCGOH3SrYM41+XKvNL0iQfM9KlYgpSjgadXx/fEKhhvOktxm8++Xw6FFeOHoOiV+lzOI8X1rsv71A== |   integrity sha512-mTpdmdZCaHCGOH3SrYM41+XKvNL0iQfM9KlYgpSjgadXx/fEKhhvOktxm8++Xw6FFeOHoOiV+lzOI8X1rsv71A== | ||||||
| @@ -492,7 +634,7 @@ | |||||||
|     csstype "^3.1.3" |     csstype "^3.1.3" | ||||||
|     prop-types "^15.8.1" |     prop-types "^15.8.1" | ||||||
|  |  | ||||||
| "@mui/system@^5.15.14 || ^6.0.0 || ^7.0.0", "@mui/system@^7.1.1": | "@mui/system@^7.1.1": | ||||||
|   version "7.1.1" |   version "7.1.1" | ||||||
|   resolved "https://registry.npmjs.org/@mui/system/-/system-7.1.1.tgz" |   resolved "https://registry.npmjs.org/@mui/system/-/system-7.1.1.tgz" | ||||||
|   integrity sha512-Kj1uhiqnj4Zo7PDjAOghtXJtNABunWvhcRU0O7RQJ7WOxeynoH6wXPcilphV8QTFtkKaip8EiNJRiCD+B3eROA== |   integrity sha512-Kj1uhiqnj4Zo7PDjAOghtXJtNABunWvhcRU0O7RQJ7WOxeynoH6wXPcilphV8QTFtkKaip8EiNJRiCD+B3eROA== | ||||||
| @@ -546,6 +688,15 @@ | |||||||
|     "@babel/runtime" "^7.27.4" |     "@babel/runtime" "^7.27.4" | ||||||
|     "@mui/utils" "^7.1.1" |     "@mui/utils" "^7.1.1" | ||||||
|  |  | ||||||
|  | "@napi-rs/wasm-runtime@^0.2.10": | ||||||
|  |   version "0.2.12" | ||||||
|  |   resolved "https://registry.yarnpkg.com/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz#3e78a8b96e6c33a6c517e1894efbd5385a7cb6f2" | ||||||
|  |   integrity sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ== | ||||||
|  |   dependencies: | ||||||
|  |     "@emnapi/core" "^1.4.3" | ||||||
|  |     "@emnapi/runtime" "^1.4.3" | ||||||
|  |     "@tybys/wasm-util" "^0.10.0" | ||||||
|  |  | ||||||
| "@nodelib/fs.scandir@2.1.5": | "@nodelib/fs.scandir@2.1.5": | ||||||
|   version "2.1.5" |   version "2.1.5" | ||||||
|   resolved "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz" |   resolved "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz" | ||||||
| @@ -554,7 +705,7 @@ | |||||||
|     "@nodelib/fs.stat" "2.0.5" |     "@nodelib/fs.stat" "2.0.5" | ||||||
|     run-parallel "^1.1.9" |     run-parallel "^1.1.9" | ||||||
|  |  | ||||||
| "@nodelib/fs.stat@^2.0.2", "@nodelib/fs.stat@2.0.5": | "@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2": | ||||||
|   version "2.0.5" |   version "2.0.5" | ||||||
|   resolved "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz" |   resolved "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz" | ||||||
|   integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== |   integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== | ||||||
| @@ -572,7 +723,7 @@ | |||||||
|   resolved "https://registry.npmjs.org/@petamoriken/float16/-/float16-3.9.2.tgz" |   resolved "https://registry.npmjs.org/@petamoriken/float16/-/float16-3.9.2.tgz" | ||||||
|   integrity sha512-VgffxawQde93xKxT3qap3OH+meZf7VaSB5Sqd4Rqc+FP5alWbpOyan/7tRbOAvynjpG3GpdtAuGU/NdhQpmrog== |   integrity sha512-VgffxawQde93xKxT3qap3OH+meZf7VaSB5Sqd4Rqc+FP5alWbpOyan/7tRbOAvynjpG3GpdtAuGU/NdhQpmrog== | ||||||
|  |  | ||||||
| "@photo-sphere-viewer/core@^5.13.2", "@photo-sphere-viewer/core@>=5.13.1": | "@photo-sphere-viewer/core@^5.13.2": | ||||||
|   version "5.13.2" |   version "5.13.2" | ||||||
|   resolved "https://registry.npmjs.org/@photo-sphere-viewer/core/-/core-5.13.2.tgz" |   resolved "https://registry.npmjs.org/@photo-sphere-viewer/core/-/core-5.13.2.tgz" | ||||||
|   integrity sha512-rL4Ey39Prx4Iyxt1f2tAqlXvqu4/ovXfUvIpLt540OpZJiFjWccs6qLywof9vuhBJ7PXHudHWCjRPce0W8kx8w== |   integrity sha512-rL4Ey39Prx4Iyxt1f2tAqlXvqu4/ovXfUvIpLt540OpZJiFjWccs6qLywof9vuhBJ7PXHudHWCjRPce0W8kx8w== | ||||||
| @@ -624,7 +775,7 @@ | |||||||
|     utility-types "^3.11.0" |     utility-types "^3.11.0" | ||||||
|     zustand "^5.0.1" |     zustand "^5.0.1" | ||||||
|  |  | ||||||
| "@react-three/fiber@^9.0.0", "@react-three/fiber@^9.1.2": | "@react-three/fiber@^9.1.2": | ||||||
|   version "9.1.2" |   version "9.1.2" | ||||||
|   resolved "https://registry.npmjs.org/@react-three/fiber/-/fiber-9.1.2.tgz" |   resolved "https://registry.npmjs.org/@react-three/fiber/-/fiber-9.1.2.tgz" | ||||||
|   integrity sha512-k8FR9yVHV9kIF3iuOD0ds5hVymXYXfgdKklqziBVod9ZEJ8uk05Zjw29J/omU3IKeUfLNAIHfxneN3TUYM4I2w== |   integrity sha512-k8FR9yVHV9kIF3iuOD0ds5hVymXYXfgdKklqziBVod9ZEJ8uk05Zjw29J/omU3IKeUfLNAIHfxneN3TUYM4I2w== | ||||||
| @@ -647,11 +798,106 @@ | |||||||
|   resolved "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.9.tgz" |   resolved "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.9.tgz" | ||||||
|   integrity sha512-e9MeMtVWo186sgvFFJOPGy7/d2j2mZhLJIdVW0C/xDluuOvymEATqz6zKsP0ZmXGzQtqlyjz5sC1sYQUoJG98w== |   integrity sha512-e9MeMtVWo186sgvFFJOPGy7/d2j2mZhLJIdVW0C/xDluuOvymEATqz6zKsP0ZmXGzQtqlyjz5sC1sYQUoJG98w== | ||||||
|  |  | ||||||
|  | "@rollup/rollup-android-arm-eabi@4.41.1": | ||||||
|  |   version "4.41.1" | ||||||
|  |   resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.41.1.tgz#f39f09f60d4a562de727c960d7b202a2cf797424" | ||||||
|  |   integrity sha512-NELNvyEWZ6R9QMkiytB4/L4zSEaBC03KIXEghptLGLZWJ6VPrL63ooZQCOnlx36aQPGhzuOMwDerC1Eb2VmrLw== | ||||||
|  |  | ||||||
|  | "@rollup/rollup-android-arm64@4.41.1": | ||||||
|  |   version "4.41.1" | ||||||
|  |   resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.41.1.tgz#d19af7e23760717f1d879d4ca3d2cd247742dff2" | ||||||
|  |   integrity sha512-DXdQe1BJ6TK47ukAoZLehRHhfKnKg9BjnQYUu9gzhI8Mwa1d2fzxA1aw2JixHVl403bwp1+/o/NhhHtxWJBgEA== | ||||||
|  |  | ||||||
| "@rollup/rollup-darwin-arm64@4.41.1": | "@rollup/rollup-darwin-arm64@4.41.1": | ||||||
|   version "4.41.1" |   version "4.41.1" | ||||||
|   resolved "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.41.1.tgz" |   resolved "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.41.1.tgz" | ||||||
|   integrity sha512-5afxvwszzdulsU2w8JKWwY8/sJOLPzf0e1bFuvcW5h9zsEg+RQAojdW0ux2zyYAz7R8HvvzKCjLNJhVq965U7w== |   integrity sha512-5afxvwszzdulsU2w8JKWwY8/sJOLPzf0e1bFuvcW5h9zsEg+RQAojdW0ux2zyYAz7R8HvvzKCjLNJhVq965U7w== | ||||||
|  |  | ||||||
|  | "@rollup/rollup-darwin-x64@4.41.1": | ||||||
|  |   version "4.41.1" | ||||||
|  |   resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.41.1.tgz#aa66d2ba1a25e609500e13bef06dc0e71cc0c0d4" | ||||||
|  |   integrity sha512-egpJACny8QOdHNNMZKf8xY0Is6gIMz+tuqXlusxquWu3F833DcMwmGM7WlvCO9sB3OsPjdC4U0wHw5FabzCGZg== | ||||||
|  |  | ||||||
|  | "@rollup/rollup-freebsd-arm64@4.41.1": | ||||||
|  |   version "4.41.1" | ||||||
|  |   resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.41.1.tgz#df10a7b6316a0ef1028c6ca71a081124c537e30d" | ||||||
|  |   integrity sha512-DBVMZH5vbjgRk3r0OzgjS38z+atlupJ7xfKIDJdZZL6sM6wjfDNo64aowcLPKIx7LMQi8vybB56uh1Ftck/Atg== | ||||||
|  |  | ||||||
|  | "@rollup/rollup-freebsd-x64@4.41.1": | ||||||
|  |   version "4.41.1" | ||||||
|  |   resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.41.1.tgz#a3fdce8a05e95b068cbcb46e4df5185e407d0c35" | ||||||
|  |   integrity sha512-3FkydeohozEskBxNWEIbPfOE0aqQgB6ttTkJ159uWOFn42VLyfAiyD9UK5mhu+ItWzft60DycIN1Xdgiy8o/SA== | ||||||
|  |  | ||||||
|  | "@rollup/rollup-linux-arm-gnueabihf@4.41.1": | ||||||
|  |   version "4.41.1" | ||||||
|  |   resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.41.1.tgz#49f766c55383bd0498014a9d76924348c2f3890c" | ||||||
|  |   integrity sha512-wC53ZNDgt0pqx5xCAgNunkTzFE8GTgdZ9EwYGVcg+jEjJdZGtq9xPjDnFgfFozQI/Xm1mh+D9YlYtl+ueswNEg== | ||||||
|  |  | ||||||
|  | "@rollup/rollup-linux-arm-musleabihf@4.41.1": | ||||||
|  |   version "4.41.1" | ||||||
|  |   resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.41.1.tgz#1d4d7d32fc557e17d52e1857817381ea365e2959" | ||||||
|  |   integrity sha512-jwKCca1gbZkZLhLRtsrka5N8sFAaxrGz/7wRJ8Wwvq3jug7toO21vWlViihG85ei7uJTpzbXZRcORotE+xyrLA== | ||||||
|  |  | ||||||
|  | "@rollup/rollup-linux-arm64-gnu@4.41.1": | ||||||
|  |   version "4.41.1" | ||||||
|  |   resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.41.1.tgz#f4fc317268441e9589edad3be8f62b6c03009bc1" | ||||||
|  |   integrity sha512-g0UBcNknsmmNQ8V2d/zD2P7WWfJKU0F1nu0k5pW4rvdb+BIqMm8ToluW/eeRmxCared5dD76lS04uL4UaNgpNA== | ||||||
|  |  | ||||||
|  | "@rollup/rollup-linux-arm64-musl@4.41.1": | ||||||
|  |   version "4.41.1" | ||||||
|  |   resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.41.1.tgz#63a1f1b0671cb17822dabae827fef0e443aebeb7" | ||||||
|  |   integrity sha512-XZpeGB5TKEZWzIrj7sXr+BEaSgo/ma/kCgrZgL0oo5qdB1JlTzIYQKel/RmhT6vMAvOdM2teYlAaOGJpJ9lahg== | ||||||
|  |  | ||||||
|  | "@rollup/rollup-linux-loongarch64-gnu@4.41.1": | ||||||
|  |   version "4.41.1" | ||||||
|  |   resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.41.1.tgz#c659b01cc6c0730b547571fc3973e1e955369f98" | ||||||
|  |   integrity sha512-bkCfDJ4qzWfFRCNt5RVV4DOw6KEgFTUZi2r2RuYhGWC8WhCA8lCAJhDeAmrM/fdiAH54m0mA0Vk2FGRPyzI+tw== | ||||||
|  |  | ||||||
|  | "@rollup/rollup-linux-powerpc64le-gnu@4.41.1": | ||||||
|  |   version "4.41.1" | ||||||
|  |   resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.41.1.tgz#612e746f9ad7e58480f964d65e0d6c3f4aae69a8" | ||||||
|  |   integrity sha512-3mr3Xm+gvMX+/8EKogIZSIEF0WUu0HL9di+YWlJpO8CQBnoLAEL/roTCxuLncEdgcfJcvA4UMOf+2dnjl4Ut1A== | ||||||
|  |  | ||||||
|  | "@rollup/rollup-linux-riscv64-gnu@4.41.1": | ||||||
|  |   version "4.41.1" | ||||||
|  |   resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.41.1.tgz#4610dbd1dcfbbae32fbc10c20ae7387acb31110c" | ||||||
|  |   integrity sha512-3rwCIh6MQ1LGrvKJitQjZFuQnT2wxfU+ivhNBzmxXTXPllewOF7JR1s2vMX/tWtUYFgphygxjqMl76q4aMotGw== | ||||||
|  |  | ||||||
|  | "@rollup/rollup-linux-riscv64-musl@4.41.1": | ||||||
|  |   version "4.41.1" | ||||||
|  |   resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.41.1.tgz#054911fab40dc83fafc21e470193c058108f19d8" | ||||||
|  |   integrity sha512-LdIUOb3gvfmpkgFZuccNa2uYiqtgZAz3PTzjuM5bH3nvuy9ty6RGc/Q0+HDFrHrizJGVpjnTZ1yS5TNNjFlklw== | ||||||
|  |  | ||||||
|  | "@rollup/rollup-linux-s390x-gnu@4.41.1": | ||||||
|  |   version "4.41.1" | ||||||
|  |   resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.41.1.tgz#98896eca8012547c7f04bd07eaa6896825f9e1a5" | ||||||
|  |   integrity sha512-oIE6M8WC9ma6xYqjvPhzZYk6NbobIURvP/lEbh7FWplcMO6gn7MM2yHKA1eC/GvYwzNKK/1LYgqzdkZ8YFxR8g== | ||||||
|  |  | ||||||
|  | "@rollup/rollup-linux-x64-gnu@4.41.1": | ||||||
|  |   version "4.41.1" | ||||||
|  |   resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.41.1.tgz#01cf56844a1e636ee80dfb364e72c2b7142ad896" | ||||||
|  |   integrity sha512-cWBOvayNvA+SyeQMp79BHPK8ws6sHSsYnK5zDcsC3Hsxr1dgTABKjMnMslPq1DvZIp6uO7kIWhiGwaTdR4Og9A== | ||||||
|  |  | ||||||
|  | "@rollup/rollup-linux-x64-musl@4.41.1": | ||||||
|  |   version "4.41.1" | ||||||
|  |   resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.41.1.tgz#e67c7531df6dff0b4c241101d4096617fbca87c3" | ||||||
|  |   integrity sha512-y5CbN44M+pUCdGDlZFzGGBSKCA4A/J2ZH4edTYSSxFg7ce1Xt3GtydbVKWLlzL+INfFIZAEg1ZV6hh9+QQf9YQ== | ||||||
|  |  | ||||||
|  | "@rollup/rollup-win32-arm64-msvc@4.41.1": | ||||||
|  |   version "4.41.1" | ||||||
|  |   resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.41.1.tgz#7eeada98444e580674de6989284e4baacd48ea65" | ||||||
|  |   integrity sha512-lZkCxIrjlJlMt1dLO/FbpZbzt6J/A8p4DnqzSa4PWqPEUUUnzXLeki/iyPLfV0BmHItlYgHUqJe+3KiyydmiNQ== | ||||||
|  |  | ||||||
|  | "@rollup/rollup-win32-ia32-msvc@4.41.1": | ||||||
|  |   version "4.41.1" | ||||||
|  |   resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.41.1.tgz#516c4b54f80587b4a390aaf4940b40870271d35d" | ||||||
|  |   integrity sha512-+psFT9+pIh2iuGsxFYYa/LhS5MFKmuivRsx9iPJWNSGbh2XVEjk90fmpUEjCnILPEPJnikAU6SFDiEUyOv90Pg== | ||||||
|  |  | ||||||
|  | "@rollup/rollup-win32-x64-msvc@4.41.1": | ||||||
|  |   version "4.41.1" | ||||||
|  |   resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.41.1.tgz#848f99b0d9936d92221bb6070baeff4db6947a30" | ||||||
|  |   integrity sha512-Wq2zpapRYLfi4aKxf2Xff0tN+7slj2d4R87WEzqw7ZLsVvO5zwYCIuEGSZYiK41+GlwUo1HiR+GdkLEJnCKTCw== | ||||||
|  |  | ||||||
| "@tailwindcss/node@4.1.8": | "@tailwindcss/node@4.1.8": | ||||||
|   version "4.1.8" |   version "4.1.8" | ||||||
|   resolved "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.8.tgz" |   resolved "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.8.tgz" | ||||||
| @@ -665,11 +911,73 @@ | |||||||
|     source-map-js "^1.2.1" |     source-map-js "^1.2.1" | ||||||
|     tailwindcss "4.1.8" |     tailwindcss "4.1.8" | ||||||
|  |  | ||||||
|  | "@tailwindcss/oxide-android-arm64@4.1.8": | ||||||
|  |   version "4.1.8" | ||||||
|  |   resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.8.tgz#4cb4b464636fc7e3154a1bb7df38a828291b3e9a" | ||||||
|  |   integrity sha512-Fbz7qni62uKYceWYvUjRqhGfZKwhZDQhlrJKGtnZfuNtHFqa8wmr+Wn74CTWERiW2hn3mN5gTpOoxWKk0jRxjg== | ||||||
|  |  | ||||||
| "@tailwindcss/oxide-darwin-arm64@4.1.8": | "@tailwindcss/oxide-darwin-arm64@4.1.8": | ||||||
|   version "4.1.8" |   version "4.1.8" | ||||||
|   resolved "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.8.tgz" |   resolved "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.8.tgz" | ||||||
|   integrity sha512-RdRvedGsT0vwVVDztvyXhKpsU2ark/BjgG0huo4+2BluxdXo8NDgzl77qh0T1nUxmM11eXwR8jA39ibvSTbi7A== |   integrity sha512-RdRvedGsT0vwVVDztvyXhKpsU2ark/BjgG0huo4+2BluxdXo8NDgzl77qh0T1nUxmM11eXwR8jA39ibvSTbi7A== | ||||||
|  |  | ||||||
|  | "@tailwindcss/oxide-darwin-x64@4.1.8": | ||||||
|  |   version "4.1.8" | ||||||
|  |   resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.8.tgz#d0f3fa4c3bde21a772e29e31c9739d91db79de12" | ||||||
|  |   integrity sha512-t6PgxjEMLp5Ovf7uMb2OFmb3kqzVTPPakWpBIFzppk4JE4ix0yEtbtSjPbU8+PZETpaYMtXvss2Sdkx8Vs4XRw== | ||||||
|  |  | ||||||
|  | "@tailwindcss/oxide-freebsd-x64@4.1.8": | ||||||
|  |   version "4.1.8" | ||||||
|  |   resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.8.tgz#545c94c941007ed1aa2e449465501b70d59cb3da" | ||||||
|  |   integrity sha512-g8C8eGEyhHTqwPStSwZNSrOlyx0bhK/V/+zX0Y+n7DoRUzyS8eMbVshVOLJTDDC+Qn9IJnilYbIKzpB9n4aBsg== | ||||||
|  |  | ||||||
|  | "@tailwindcss/oxide-linux-arm-gnueabihf@4.1.8": | ||||||
|  |   version "4.1.8" | ||||||
|  |   resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.8.tgz#e1bdbf63a179081669b8cd1c9523889774760eb9" | ||||||
|  |   integrity sha512-Jmzr3FA4S2tHhaC6yCjac3rGf7hG9R6Gf2z9i9JFcuyy0u79HfQsh/thifbYTF2ic82KJovKKkIB6Z9TdNhCXQ== | ||||||
|  |  | ||||||
|  | "@tailwindcss/oxide-linux-arm64-gnu@4.1.8": | ||||||
|  |   version "4.1.8" | ||||||
|  |   resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.8.tgz#8d28093bbd43bdae771a2dcca720e926baa57093" | ||||||
|  |   integrity sha512-qq7jXtO1+UEtCmCeBBIRDrPFIVI4ilEQ97qgBGdwXAARrUqSn/L9fUrkb1XP/mvVtoVeR2bt/0L77xx53bPZ/Q== | ||||||
|  |  | ||||||
|  | "@tailwindcss/oxide-linux-arm64-musl@4.1.8": | ||||||
|  |   version "4.1.8" | ||||||
|  |   resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.8.tgz#cc6cece814d813885ead9cd8b9d55aeb3db56c97" | ||||||
|  |   integrity sha512-O6b8QesPbJCRshsNApsOIpzKt3ztG35gfX9tEf4arD7mwNinsoCKxkj8TgEE0YRjmjtO3r9FlJnT/ENd9EVefQ== | ||||||
|  |  | ||||||
|  | "@tailwindcss/oxide-linux-x64-gnu@4.1.8": | ||||||
|  |   version "4.1.8" | ||||||
|  |   resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.8.tgz#4cac14fa71382574773fb7986d9f0681ad89e3de" | ||||||
|  |   integrity sha512-32iEXX/pXwikshNOGnERAFwFSfiltmijMIAbUhnNyjFr3tmWmMJWQKU2vNcFX0DACSXJ3ZWcSkzNbaKTdngH6g== | ||||||
|  |  | ||||||
|  | "@tailwindcss/oxide-linux-x64-musl@4.1.8": | ||||||
|  |   version "4.1.8" | ||||||
|  |   resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.8.tgz#e085f1ccbc8f97625773a6a3afc2a6f88edf59da" | ||||||
|  |   integrity sha512-s+VSSD+TfZeMEsCaFaHTaY5YNj3Dri8rST09gMvYQKwPphacRG7wbuQ5ZJMIJXN/puxPcg/nU+ucvWguPpvBDg== | ||||||
|  |  | ||||||
|  | "@tailwindcss/oxide-wasm32-wasi@4.1.8": | ||||||
|  |   version "4.1.8" | ||||||
|  |   resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.8.tgz#c5e19fffe67f25cabf12a357bba4e87128151ea0" | ||||||
|  |   integrity sha512-CXBPVFkpDjM67sS1psWohZ6g/2/cd+cq56vPxK4JeawelxwK4YECgl9Y9TjkE2qfF+9/s1tHHJqrC4SS6cVvSg== | ||||||
|  |   dependencies: | ||||||
|  |     "@emnapi/core" "^1.4.3" | ||||||
|  |     "@emnapi/runtime" "^1.4.3" | ||||||
|  |     "@emnapi/wasi-threads" "^1.0.2" | ||||||
|  |     "@napi-rs/wasm-runtime" "^0.2.10" | ||||||
|  |     "@tybys/wasm-util" "^0.9.0" | ||||||
|  |     tslib "^2.8.0" | ||||||
|  |  | ||||||
|  | "@tailwindcss/oxide-win32-arm64-msvc@4.1.8": | ||||||
|  |   version "4.1.8" | ||||||
|  |   resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.8.tgz#77521f23f91604c587736927fd2cb526667b7344" | ||||||
|  |   integrity sha512-7GmYk1n28teDHUjPlIx4Z6Z4hHEgvP5ZW2QS9ygnDAdI/myh3HTHjDqtSqgu1BpRoI4OiLx+fThAyA1JePoENA== | ||||||
|  |  | ||||||
|  | "@tailwindcss/oxide-win32-x64-msvc@4.1.8": | ||||||
|  |   version "4.1.8" | ||||||
|  |   resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.8.tgz#55c876ab35f8779d1dceec61483cd9834d7365ac" | ||||||
|  |   integrity sha512-fou+U20j+Jl0EHwK92spoWISON2OBnCazIc038Xj2TdweYV33ZRkS9nwqiUi2d/Wba5xg5UoHfvynnb/UB49cQ== | ||||||
|  |  | ||||||
| "@tailwindcss/oxide@4.1.8": | "@tailwindcss/oxide@4.1.8": | ||||||
|   version "4.1.8" |   version "4.1.8" | ||||||
|   resolved "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.8.tgz" |   resolved "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.8.tgz" | ||||||
| @@ -715,6 +1023,20 @@ | |||||||
|   resolved "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.3.tgz" |   resolved "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.3.tgz" | ||||||
|   integrity sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA== |   integrity sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA== | ||||||
|  |  | ||||||
|  | "@tybys/wasm-util@^0.10.0": | ||||||
|  |   version "0.10.1" | ||||||
|  |   resolved "https://registry.yarnpkg.com/@tybys/wasm-util/-/wasm-util-0.10.1.tgz#ecddd3205cf1e2d5274649ff0eedd2991ed7f414" | ||||||
|  |   integrity sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg== | ||||||
|  |   dependencies: | ||||||
|  |     tslib "^2.4.0" | ||||||
|  |  | ||||||
|  | "@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" | ||||||
|  |   integrity sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw== | ||||||
|  |   dependencies: | ||||||
|  |     tslib "^2.4.0" | ||||||
|  |  | ||||||
| "@types/babel__core@^7.20.5": | "@types/babel__core@^7.20.5": | ||||||
|   version "7.20.5" |   version "7.20.5" | ||||||
|   resolved "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz" |   resolved "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz" | ||||||
| @@ -784,7 +1106,7 @@ | |||||||
|   dependencies: |   dependencies: | ||||||
|     "@types/estree" "*" |     "@types/estree" "*" | ||||||
|  |  | ||||||
| "@types/estree@*", "@types/estree@^1.0.0", "@types/estree@^1.0.6", "@types/estree@1.0.7": | "@types/estree@*", "@types/estree@1.0.7", "@types/estree@^1.0.0", "@types/estree@^1.0.6": | ||||||
|   version "1.0.7" |   version "1.0.7" | ||||||
|   resolved "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz" |   resolved "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz" | ||||||
|   integrity sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ== |   integrity sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ== | ||||||
| @@ -818,7 +1140,7 @@ | |||||||
|   resolved "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz" |   resolved "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz" | ||||||
|   integrity sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA== |   integrity sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA== | ||||||
|  |  | ||||||
| "@types/node@^18.0.0 || ^20.0.0 || >=22.0.0", "@types/node@^22.15.24": | "@types/node@^22.15.24": | ||||||
|   version "22.15.24" |   version "22.15.24" | ||||||
|   resolved "https://registry.npmjs.org/@types/node/-/node-22.15.24.tgz" |   resolved "https://registry.npmjs.org/@types/node/-/node-22.15.24.tgz" | ||||||
|   integrity sha512-w9CZGm9RDjzTh/D+hFwlBJ3ziUaVw7oufKA3vOFSOZlzmW9AkZnfjPb+DLnrV6qtgL/LNmP0/2zBNCFHL3F0ng== |   integrity sha512-w9CZGm9RDjzTh/D+hFwlBJ3ziUaVw7oufKA3vOFSOZlzmW9AkZnfjPb+DLnrV6qtgL/LNmP0/2zBNCFHL3F0ng== | ||||||
| @@ -860,7 +1182,7 @@ | |||||||
|   resolved "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz" |   resolved "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz" | ||||||
|   integrity sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w== |   integrity sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w== | ||||||
|  |  | ||||||
| "@types/react@*", "@types/react@^17.0.0 || ^18.0.0 || ^19.0.0", "@types/react@^18.2.25 || ^19", "@types/react@^19.0.0", "@types/react@^19.1.2", "@types/react@>=16.8", "@types/react@>=18", "@types/react@>=18.0.0": | "@types/react@^19.1.2": | ||||||
|   version "19.1.6" |   version "19.1.6" | ||||||
|   resolved "https://registry.npmjs.org/@types/react/-/react-19.1.6.tgz" |   resolved "https://registry.npmjs.org/@types/react/-/react-19.1.6.tgz" | ||||||
|   integrity sha512-JeG0rEWak0N6Itr6QUx+X60uQmN+5t3j9r/OVDtWzFXKaj6kD1BwJzOksD0FF6iWxZlbE1kB0q9vtnU2ekqa1Q== |   integrity sha512-JeG0rEWak0N6Itr6QUx+X60uQmN+5t3j9r/OVDtWzFXKaj6kD1BwJzOksD0FF6iWxZlbE1kB0q9vtnU2ekqa1Q== | ||||||
| @@ -879,7 +1201,7 @@ | |||||||
|   dependencies: |   dependencies: | ||||||
|     "@types/estree" "*" |     "@types/estree" "*" | ||||||
|  |  | ||||||
| "@types/three@*", "@types/three@>=0.134.0": | "@types/three@*": | ||||||
|   version "0.177.0" |   version "0.177.0" | ||||||
|   resolved "https://registry.npmjs.org/@types/three/-/three-0.177.0.tgz" |   resolved "https://registry.npmjs.org/@types/three/-/three-0.177.0.tgz" | ||||||
|   integrity sha512-/ZAkn4OLUijKQySNci47lFO+4JLE1TihEjsGWPUT+4jWqxtwOPPEwJV1C3k5MEx0mcBPCdkFjzRzDOnHEI1R+A== |   integrity sha512-/ZAkn4OLUijKQySNci47lFO+4JLE1TihEjsGWPUT+4jWqxtwOPPEwJV1C3k5MEx0mcBPCdkFjzRzDOnHEI1R+A== | ||||||
| @@ -927,7 +1249,7 @@ | |||||||
|     natural-compare "^1.4.0" |     natural-compare "^1.4.0" | ||||||
|     ts-api-utils "^2.1.0" |     ts-api-utils "^2.1.0" | ||||||
|  |  | ||||||
| "@typescript-eslint/parser@^8.33.0", "@typescript-eslint/parser@8.33.0": | "@typescript-eslint/parser@8.33.0": | ||||||
|   version "8.33.0" |   version "8.33.0" | ||||||
|   resolved "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.33.0.tgz" |   resolved "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.33.0.tgz" | ||||||
|   integrity sha512-JaehZvf6m0yqYp34+RVnihBAChkqeH+tqqhS0GuX1qgPpwLvmTPheKEs6OeCK6hVJgXZHJ2vbjnC9j119auStQ== |   integrity sha512-JaehZvf6m0yqYp34+RVnihBAChkqeH+tqqhS0GuX1qgPpwLvmTPheKEs6OeCK6hVJgXZHJ2vbjnC9j119auStQ== | ||||||
| @@ -955,7 +1277,7 @@ | |||||||
|     "@typescript-eslint/types" "8.33.0" |     "@typescript-eslint/types" "8.33.0" | ||||||
|     "@typescript-eslint/visitor-keys" "8.33.0" |     "@typescript-eslint/visitor-keys" "8.33.0" | ||||||
|  |  | ||||||
| "@typescript-eslint/tsconfig-utils@^8.33.0", "@typescript-eslint/tsconfig-utils@8.33.0": | "@typescript-eslint/tsconfig-utils@8.33.0", "@typescript-eslint/tsconfig-utils@^8.33.0": | ||||||
|   version "8.33.0" |   version "8.33.0" | ||||||
|   resolved "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.33.0.tgz" |   resolved "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.33.0.tgz" | ||||||
|   integrity sha512-sTkETlbqhEoiFmGr1gsdq5HyVbSOF0145SYDJ/EQmXHtKViCaGvnyLqWFFHtEXoS0J1yU8Wyou2UGmgW88fEug== |   integrity sha512-sTkETlbqhEoiFmGr1gsdq5HyVbSOF0145SYDJ/EQmXHtKViCaGvnyLqWFFHtEXoS0J1yU8Wyou2UGmgW88fEug== | ||||||
| @@ -970,7 +1292,7 @@ | |||||||
|     debug "^4.3.4" |     debug "^4.3.4" | ||||||
|     ts-api-utils "^2.1.0" |     ts-api-utils "^2.1.0" | ||||||
|  |  | ||||||
| "@typescript-eslint/types@^8.33.0", "@typescript-eslint/types@8.33.0": | "@typescript-eslint/types@8.33.0", "@typescript-eslint/types@^8.33.0": | ||||||
|   version "8.33.0" |   version "8.33.0" | ||||||
|   resolved "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.33.0.tgz" |   resolved "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.33.0.tgz" | ||||||
|   integrity sha512-DKuXOKpM5IDT1FA2g9x9x1Ug81YuKrzf4mYX8FAVSNu5Wo/LELHWQyM1pQaDkI42bX15PWl0vNPt1uGiIFUOpg== |   integrity sha512-DKuXOKpM5IDT1FA2g9x9x1Ug81YuKrzf4mYX8FAVSNu5Wo/LELHWQyM1pQaDkI42bX15PWl0vNPt1uGiIFUOpg== | ||||||
| @@ -1053,7 +1375,7 @@ acorn-jsx@^5.3.2: | |||||||
|   resolved "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz" |   resolved "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz" | ||||||
|   integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== |   integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== | ||||||
|  |  | ||||||
| "acorn@^6.0.0 || ^7.0.0 || ^8.0.0", acorn@^8.14.0: | acorn@^8.14.0: | ||||||
|   version "8.14.1" |   version "8.14.1" | ||||||
|   resolved "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz" |   resolved "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz" | ||||||
|   integrity sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg== |   integrity sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg== | ||||||
| @@ -1152,7 +1474,7 @@ braces@^3.0.3: | |||||||
|   dependencies: |   dependencies: | ||||||
|     fill-range "^7.1.1" |     fill-range "^7.1.1" | ||||||
|  |  | ||||||
| browserslist@^4.24.0, "browserslist@>= 4.21.0": | browserslist@^4.24.0: | ||||||
|   version "4.24.5" |   version "4.24.5" | ||||||
|   resolved "https://registry.npmjs.org/browserslist/-/browserslist-4.24.5.tgz" |   resolved "https://registry.npmjs.org/browserslist/-/browserslist-4.24.5.tgz" | ||||||
|   integrity sha512-FDToo4Wo82hIdgc1CQ+NQD0hEhmpPjrZ3hiUgwgOG6IuTdlpr8jdjyG24P6cNP1yJpTLzS5OcGgSw0xmDU1/Tw== |   integrity sha512-FDToo4Wo82hIdgc1CQ+NQD0hEhmpPjrZ3hiUgwgOG6IuTdlpr8jdjyG24P6cNP1yJpTLzS5OcGgSw0xmDU1/Tw== | ||||||
| @@ -1411,7 +1733,7 @@ earcut@^3.0.0, earcut@^3.0.1: | |||||||
|   resolved "https://registry.npmjs.org/earcut/-/earcut-3.0.1.tgz" |   resolved "https://registry.npmjs.org/earcut/-/earcut-3.0.1.tgz" | ||||||
|   integrity sha512-0l1/0gOjESMeQyYaK5IDiPNvFeu93Z/cO0TjZh9eZ1vyCtZnA7KMZ8rQggpsJHIbGSdrqYq9OhuveadOVHCshw== |   integrity sha512-0l1/0gOjESMeQyYaK5IDiPNvFeu93Z/cO0TjZh9eZ1vyCtZnA7KMZ8rQggpsJHIbGSdrqYq9OhuveadOVHCshw== | ||||||
|  |  | ||||||
| easymde@^2.20.0, "easymde@>= 2.0.0 < 3.0.0": | easymde@^2.20.0: | ||||||
|   version "2.20.0" |   version "2.20.0" | ||||||
|   resolved "https://registry.npmjs.org/easymde/-/easymde-2.20.0.tgz" |   resolved "https://registry.npmjs.org/easymde/-/easymde-2.20.0.tgz" | ||||||
|   integrity sha512-V1Z5f92TfR42Na852OWnIZMbM7zotWQYTddNaLYZFVKj7APBbyZ3FYJ27gBw2grMW3R6Qdv9J8n5Ij7XRSIgXQ== |   integrity sha512-V1Z5f92TfR42Na852OWnIZMbM7zotWQYTddNaLYZFVKj7APBbyZ3FYJ27gBw2grMW3R6Qdv9J8n5Ij7XRSIgXQ== | ||||||
| @@ -1543,7 +1865,7 @@ eslint-visitor-keys@^4.2.0: | |||||||
|   resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz" |   resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz" | ||||||
|   integrity sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw== |   integrity sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw== | ||||||
|  |  | ||||||
| "eslint@^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0", "eslint@^6.0.0 || ^7.0.0 || >=8.0.0", "eslint@^8.57.0 || ^9.0.0", eslint@^9.25.0, eslint@>=8.40: | eslint@^9.25.0: | ||||||
|   version "9.27.0" |   version "9.27.0" | ||||||
|   resolved "https://registry.npmjs.org/eslint/-/eslint-9.27.0.tgz" |   resolved "https://registry.npmjs.org/eslint/-/eslint-9.27.0.tgz" | ||||||
|   integrity sha512-ixRawFQuMB9DZ7fjU3iGGganFDp3+45bPOdaRurcFHSXO1e/sYwUX/FtQZpLZJR6SjMoJH8hR2pPEAfDyCoU2Q== |   integrity sha512-ixRawFQuMB9DZ7fjU3iGGganFDp3+45bPOdaRurcFHSXO1e/sYwUX/FtQZpLZJR6SjMoJH8hR2pPEAfDyCoU2Q== | ||||||
| @@ -2111,7 +2433,7 @@ its-fine@^2.0.0: | |||||||
|   dependencies: |   dependencies: | ||||||
|     "@types/react-reconciler" "^0.28.9" |     "@types/react-reconciler" "^0.28.9" | ||||||
|  |  | ||||||
| jiti@*, jiti@^2.4.2, jiti@>=1.21.0: | jiti@^2.4.2: | ||||||
|   version "2.4.2" |   version "2.4.2" | ||||||
|   resolved "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz" |   resolved "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz" | ||||||
|   integrity sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A== |   integrity sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A== | ||||||
| @@ -2195,7 +2517,52 @@ lightningcss-darwin-arm64@1.30.1: | |||||||
|   resolved "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.1.tgz" |   resolved "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.1.tgz" | ||||||
|   integrity sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ== |   integrity sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ== | ||||||
|  |  | ||||||
| lightningcss@^1.21.0, lightningcss@1.30.1: | lightningcss-darwin-x64@1.30.1: | ||||||
|  |   version "1.30.1" | ||||||
|  |   resolved "https://registry.yarnpkg.com/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.1.tgz#e81105d3fd6330860c15fe860f64d39cff5fbd22" | ||||||
|  |   integrity sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA== | ||||||
|  |  | ||||||
|  | lightningcss-freebsd-x64@1.30.1: | ||||||
|  |   version "1.30.1" | ||||||
|  |   resolved "https://registry.yarnpkg.com/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.1.tgz#a0e732031083ff9d625c5db021d09eb085af8be4" | ||||||
|  |   integrity sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig== | ||||||
|  |  | ||||||
|  | lightningcss-linux-arm-gnueabihf@1.30.1: | ||||||
|  |   version "1.30.1" | ||||||
|  |   resolved "https://registry.yarnpkg.com/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.1.tgz#1f5ecca6095528ddb649f9304ba2560c72474908" | ||||||
|  |   integrity sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q== | ||||||
|  |  | ||||||
|  | lightningcss-linux-arm64-gnu@1.30.1: | ||||||
|  |   version "1.30.1" | ||||||
|  |   resolved "https://registry.yarnpkg.com/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.1.tgz#eee7799726103bffff1e88993df726f6911ec009" | ||||||
|  |   integrity sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw== | ||||||
|  |  | ||||||
|  | lightningcss-linux-arm64-musl@1.30.1: | ||||||
|  |   version "1.30.1" | ||||||
|  |   resolved "https://registry.yarnpkg.com/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.1.tgz#f2e4b53f42892feeef8f620cbb889f7c064a7dfe" | ||||||
|  |   integrity sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ== | ||||||
|  |  | ||||||
|  | lightningcss-linux-x64-gnu@1.30.1: | ||||||
|  |   version "1.30.1" | ||||||
|  |   resolved "https://registry.yarnpkg.com/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.1.tgz#2fc7096224bc000ebb97eea94aea248c5b0eb157" | ||||||
|  |   integrity sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw== | ||||||
|  |  | ||||||
|  | lightningcss-linux-x64-musl@1.30.1: | ||||||
|  |   version "1.30.1" | ||||||
|  |   resolved "https://registry.yarnpkg.com/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.1.tgz#66dca2b159fd819ea832c44895d07e5b31d75f26" | ||||||
|  |   integrity sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ== | ||||||
|  |  | ||||||
|  | lightningcss-win32-arm64-msvc@1.30.1: | ||||||
|  |   version "1.30.1" | ||||||
|  |   resolved "https://registry.yarnpkg.com/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.1.tgz#7d8110a19d7c2d22bfdf2f2bb8be68e7d1b69039" | ||||||
|  |   integrity sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA== | ||||||
|  |  | ||||||
|  | lightningcss-win32-x64-msvc@1.30.1: | ||||||
|  |   version "1.30.1" | ||||||
|  |   resolved "https://registry.yarnpkg.com/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.1.tgz#fd7dd008ea98494b85d24b4bea016793f2e0e352" | ||||||
|  |   integrity sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg== | ||||||
|  |  | ||||||
|  | lightningcss@1.30.1: | ||||||
|   version "1.30.1" |   version "1.30.1" | ||||||
|   resolved "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz" |   resolved "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz" | ||||||
|   integrity sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg== |   integrity sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg== | ||||||
| @@ -2658,7 +3025,7 @@ mobx-react-lite@^4.1.0: | |||||||
|   dependencies: |   dependencies: | ||||||
|     use-sync-external-store "^1.4.0" |     use-sync-external-store "^1.4.0" | ||||||
|  |  | ||||||
| mobx@^6.13.7, mobx@^6.9.0: | mobx@^6.13.7: | ||||||
|   version "6.13.7" |   version "6.13.7" | ||||||
|   resolved "https://registry.npmjs.org/mobx/-/mobx-6.13.7.tgz" |   resolved "https://registry.npmjs.org/mobx/-/mobx-6.13.7.tgz" | ||||||
|   integrity sha512-aChaVU/DO5aRPmk1GX8L+whocagUUpBQqoPtJk+cm7UOXUk87J4PeWCh6nNmTTIfEhiR9DI/+FnA8dln/hTK7g== |   integrity sha512-aChaVU/DO5aRPmk1GX8L+whocagUUpBQqoPtJk+cm7UOXUk87J4PeWCh6nNmTTIfEhiR9DI/+FnA8dln/hTK7g== | ||||||
| @@ -2822,12 +3189,12 @@ picomatch@^2.3.1: | |||||||
|   resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz" |   resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz" | ||||||
|   integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== |   integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== | ||||||
|  |  | ||||||
| "picomatch@^3 || ^4", picomatch@^4.0.2: | picomatch@^4.0.2: | ||||||
|   version "4.0.2" |   version "4.0.2" | ||||||
|   resolved "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz" |   resolved "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz" | ||||||
|   integrity sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg== |   integrity sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg== | ||||||
|  |  | ||||||
| pixi.js@^8.10.1, pixi.js@^8.2.6: | pixi.js@^8.10.1: | ||||||
|   version "8.11.0" |   version "8.11.0" | ||||||
|   resolved "https://registry.npmjs.org/pixi.js/-/pixi.js-8.11.0.tgz" |   resolved "https://registry.npmjs.org/pixi.js/-/pixi.js-8.11.0.tgz" | ||||||
|   integrity sha512-dyuThzncsgEgJZnvd/A/5x6IkUERbK+phXqUQrI+0C6WE+8xqGH5VChRTLecemhgZF0kQ+gZOM3tJTX9937xpg== |   integrity sha512-dyuThzncsgEgJZnvd/A/5x6IkUERbK+phXqUQrI+0C6WE+8xqGH5VChRTLecemhgZF0kQ+gZOM3tJTX9937xpg== | ||||||
| @@ -2883,7 +3250,7 @@ promise-worker-transferable@^1.0.4: | |||||||
|     is-promise "^2.1.0" |     is-promise "^2.1.0" | ||||||
|     lie "^3.0.2" |     lie "^3.0.2" | ||||||
|  |  | ||||||
| prop-types@^15.5.4, prop-types@^15.6.2, prop-types@^15.8.1: | prop-types@^15.6.2, prop-types@^15.8.1: | ||||||
|   version "15.8.1" |   version "15.8.1" | ||||||
|   resolved "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz" |   resolved "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz" | ||||||
|   integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== |   integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== | ||||||
| @@ -2949,7 +3316,7 @@ react-colorful@^5.6.1: | |||||||
|   resolved "https://registry.npmjs.org/react-colorful/-/react-colorful-5.6.1.tgz" |   resolved "https://registry.npmjs.org/react-colorful/-/react-colorful-5.6.1.tgz" | ||||||
|   integrity sha512-1exovf0uGTGyq5mXQT0zgQ80uvj2PCwvF8zY1RN9/vbJVSjSo3fsB/4L3ObbF7u70NduSiK4xu4Y6q1MHoUGEw== |   integrity sha512-1exovf0uGTGyq5mXQT0zgQ80uvj2PCwvF8zY1RN9/vbJVSjSo3fsB/4L3ObbF7u70NduSiK4xu4Y6q1MHoUGEw== | ||||||
|  |  | ||||||
| "react-dom@^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom@^18 || ^19", "react-dom@^18.0.0 || ^19.0.0", react-dom@^19, react-dom@^19.0.0, react-dom@^19.1.0, react-dom@>=16.0.0, react-dom@>=16.13, react-dom@>=16.6.0, react-dom@>=16.8.0, react-dom@>=16.8.2, react-dom@>=18: | react-dom@^19.1.0: | ||||||
|   version "19.1.0" |   version "19.1.0" | ||||||
|   resolved "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz" |   resolved "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz" | ||||||
|   integrity sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g== |   integrity sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g== | ||||||
| @@ -2999,7 +3366,7 @@ react-photo-sphere-viewer@^6.2.3: | |||||||
|   dependencies: |   dependencies: | ||||||
|     eventemitter3 "^5.0.1" |     eventemitter3 "^5.0.1" | ||||||
|  |  | ||||||
| react-reconciler@^0.31.0, react-reconciler@0.31.0: | react-reconciler@0.31.0, react-reconciler@^0.31.0: | ||||||
|   version "0.31.0" |   version "0.31.0" | ||||||
|   resolved "https://registry.npmjs.org/react-reconciler/-/react-reconciler-0.31.0.tgz" |   resolved "https://registry.npmjs.org/react-reconciler/-/react-reconciler-0.31.0.tgz" | ||||||
|   integrity sha512-7Ob7Z+URmesIsIVRjnLoDGwBEG/tVitidU0nMsqX/eeJaLY89RISO/10ERe0MqmzuKUUB1rmY+h1itMbUHg9BQ== |   integrity sha512-7Ob7Z+URmesIsIVRjnLoDGwBEG/tVitidU0nMsqX/eeJaLY89RISO/10ERe0MqmzuKUUB1rmY+h1itMbUHg9BQ== | ||||||
| @@ -3063,12 +3430,12 @@ react-use-measure@^2.1.7: | |||||||
|   resolved "https://registry.npmjs.org/react-use-measure/-/react-use-measure-2.1.7.tgz" |   resolved "https://registry.npmjs.org/react-use-measure/-/react-use-measure-2.1.7.tgz" | ||||||
|   integrity sha512-KrvcAo13I/60HpwGO5jpW7E9DfusKyLPLvuHlUyP5zqnmAPhNc6qTRjUQrdTADl0lpPpDVU2/Gg51UlOGHXbdg== |   integrity sha512-KrvcAo13I/60HpwGO5jpW7E9DfusKyLPLvuHlUyP5zqnmAPhNc6qTRjUQrdTADl0lpPpDVU2/Gg51UlOGHXbdg== | ||||||
|  |  | ||||||
| "react@^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react@^16.8.0 || ^17 || ^18 || ^19", "react@^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react@^17.0.0 || ^18.0.0 || ^19.0.0", "react@^18 || ^19", "react@^18.0 || ^19", "react@^18.0.0 || ^19.0.0", react@^19, react@^19.0.0, react@^19.1.0, "react@>= 16.8 || 18.0.0", "react@>= 16.8.0", react@>=16.0.0, react@>=16.13, react@>=16.6.0, react@>=16.8, react@>=16.8.0, react@>=16.8.2, react@>=17.0, react@>=18, react@>=18.0.0, react@>=19.0.0: | react@^19.1.0: | ||||||
|   version "19.1.0" |   version "19.1.0" | ||||||
|   resolved "https://registry.npmjs.org/react/-/react-19.1.0.tgz" |   resolved "https://registry.npmjs.org/react/-/react-19.1.0.tgz" | ||||||
|   integrity sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg== |   integrity sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg== | ||||||
|  |  | ||||||
| redux@^5.0.0, redux@^5.0.1: | redux@^5.0.1: | ||||||
|   version "5.0.1" |   version "5.0.1" | ||||||
|   resolved "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz" |   resolved "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz" | ||||||
|   integrity sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w== |   integrity sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w== | ||||||
| @@ -3289,7 +3656,7 @@ suspend-react@^0.1.3: | |||||||
|   resolved "https://registry.npmjs.org/suspend-react/-/suspend-react-0.1.3.tgz" |   resolved "https://registry.npmjs.org/suspend-react/-/suspend-react-0.1.3.tgz" | ||||||
|   integrity sha512-aqldKgX9aZqpoDp3e8/BZ8Dm7x1pJl+qI3ZKxDN0i/IQTWUwBx/ManmlVJ3wowqbno6c2bmiIfs+Um6LbsjJyQ== |   integrity sha512-aqldKgX9aZqpoDp3e8/BZ8Dm7x1pJl+qI3ZKxDN0i/IQTWUwBx/ManmlVJ3wowqbno6c2bmiIfs+Um6LbsjJyQ== | ||||||
|  |  | ||||||
| tailwindcss@^4.1.8, "tailwindcss@>=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1", tailwindcss@4.1.8: | tailwindcss@4.1.8, tailwindcss@^4.1.8: | ||||||
|   version "4.1.8" |   version "4.1.8" | ||||||
|   resolved "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.8.tgz" |   resolved "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.8.tgz" | ||||||
|   integrity sha512-kjeW8gjdxasbmFKpVGrGd5T4i40mV5J2Rasw48QARfYeQ8YS9x02ON9SFWax3Qf616rt4Cp3nVNIj6Hd1mP3og== |   integrity sha512-kjeW8gjdxasbmFKpVGrGd5T4i40mV5J2Rasw48QARfYeQ8YS9x02ON9SFWax3Qf616rt4Cp3nVNIj6Hd1mP3og== | ||||||
| @@ -3338,7 +3705,7 @@ three@^0.175.0: | |||||||
|   resolved "https://registry.npmjs.org/three/-/three-0.175.0.tgz" |   resolved "https://registry.npmjs.org/three/-/three-0.175.0.tgz" | ||||||
|   integrity sha512-nNE3pnTHxXN/Phw768u0Grr7W4+rumGg/H6PgeseNJojkJtmeHJfZWi41Gp2mpXl1pg1pf1zjwR4McM1jTqkpg== |   integrity sha512-nNE3pnTHxXN/Phw768u0Grr7W4+rumGg/H6PgeseNJojkJtmeHJfZWi41Gp2mpXl1pg1pf1zjwR4McM1jTqkpg== | ||||||
|  |  | ||||||
| three@^0.177.0, "three@>= 0.159.0", three@>=0.125.0, three@>=0.126.1, three@>=0.128.0, three@>=0.134.0, three@>=0.137, three@>=0.156, three@>=0.159: | three@^0.177.0: | ||||||
|   version "0.177.0" |   version "0.177.0" | ||||||
|   resolved "https://registry.npmjs.org/three/-/three-0.177.0.tgz" |   resolved "https://registry.npmjs.org/three/-/three-0.177.0.tgz" | ||||||
|   integrity sha512-EiXv5/qWAaGI+Vz2A+JfavwYCMdGjxVsrn3oBwllUoqYeaBO75J63ZfyaQKoiLrqNHoTlUc6PFgMXnS0kI45zg== |   integrity sha512-EiXv5/qWAaGI+Vz2A+JfavwYCMdGjxVsrn3oBwllUoqYeaBO75J63ZfyaQKoiLrqNHoTlUc6PFgMXnS0kI45zg== | ||||||
| @@ -3398,9 +3765,9 @@ ts-api-utils@^2.1.0: | |||||||
|   resolved "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz" |   resolved "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz" | ||||||
|   integrity sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ== |   integrity sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ== | ||||||
|  |  | ||||||
| tslib@^2.7.0: | tslib@^2.4.0, tslib@^2.7.0, tslib@^2.8.0: | ||||||
|   version "2.8.1" |   version "2.8.1" | ||||||
|   resolved "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz" |   resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" | ||||||
|   integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== |   integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== | ||||||
|  |  | ||||||
| tunnel-rat@^0.1.2: | tunnel-rat@^0.1.2: | ||||||
| @@ -3426,7 +3793,7 @@ typescript-eslint@^8.30.1: | |||||||
|     "@typescript-eslint/parser" "8.33.0" |     "@typescript-eslint/parser" "8.33.0" | ||||||
|     "@typescript-eslint/utils" "8.33.0" |     "@typescript-eslint/utils" "8.33.0" | ||||||
|  |  | ||||||
| typescript@>=4.8.4, "typescript@>=4.8.4 <5.9.0", typescript@~5.8.3: | typescript@~5.8.3: | ||||||
|   version "5.8.3" |   version "5.8.3" | ||||||
|   resolved "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz" |   resolved "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz" | ||||||
|   integrity sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ== |   integrity sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ== | ||||||
| @@ -3507,7 +3874,7 @@ uri-js@^4.2.2: | |||||||
|   dependencies: |   dependencies: | ||||||
|     punycode "^2.1.0" |     punycode "^2.1.0" | ||||||
|  |  | ||||||
| use-sync-external-store@^1.2.2, use-sync-external-store@^1.4.0, use-sync-external-store@^1.5.0, use-sync-external-store@>=1.2.0: | use-sync-external-store@^1.2.2, use-sync-external-store@^1.4.0, use-sync-external-store@^1.5.0: | ||||||
|   version "1.5.0" |   version "1.5.0" | ||||||
|   resolved "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz" |   resolved "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz" | ||||||
|   integrity sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A== |   integrity sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A== | ||||||
| @@ -3553,7 +3920,7 @@ vfile@^6.0.0: | |||||||
|     "@types/unist" "^3.0.0" |     "@types/unist" "^3.0.0" | ||||||
|     vfile-message "^4.0.0" |     vfile-message "^4.0.0" | ||||||
|  |  | ||||||
| "vite@^4.2.0 || ^5.0.0 || ^6.0.0", "vite@^5.2.0 || ^6", vite@^6.3.5: | vite@^6.3.5: | ||||||
|   version "6.3.5" |   version "6.3.5" | ||||||
|   resolved "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz" |   resolved "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz" | ||||||
|   integrity sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ== |   integrity sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ== | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user