From 7382a850828ced9067961aedd4896952dc25a5c0 Mon Sep 17 00:00:00 2001 From: itoshi Date: Thu, 2 Oct 2025 04:38:15 +0300 Subject: [PATCH] feat: Update city logic in map page with scrollbar --- CITY_SELECTOR_FEATURE.md | 62 -- src/pages/MapPage/index.tsx | 855 +++++++++++------- .../SightTabs/CreateInformationTab/index.tsx | 2 +- 3 files changed, 510 insertions(+), 409 deletions(-) delete mode 100644 CITY_SELECTOR_FEATURE.md diff --git a/CITY_SELECTOR_FEATURE.md b/CITY_SELECTOR_FEATURE.md deleted file mode 100644 index a1aea66..0000000 --- a/CITY_SELECTOR_FEATURE.md +++ /dev/null @@ -1,62 +0,0 @@ -# Селектор городов - -## Описание функциональности - -Добавлена функциональность выбора города в админ-панели "Белые ночи": - -### Основные возможности: - -1. **Селектор городов в шапке приложения** - - - Расположен рядом с именем пользователя в верхней части приложения - - Показывает список всех доступных городов - - Имеет иконку MapPin для лучшего UX - -2. **Сохранение в localStorage** - - - Выбранный город автоматически сохраняется в localStorage - - При перезагрузке страницы выбранный город восстанавливается - -3. **Автоматическое использование в формах** - - При создании новой станции выбранный город автоматически подставляется - - При создании нового перевозчика выбранный город автоматически подставляется - - Пользователь может изменить город в форме при необходимости - -### Технические детали: - -#### Новые компоненты и сторы: - -- `SelectedCityStore` - стор для управления выбранным городом -- `CitySelector` - компонент селектора городов -- `useSelectedCity` - хук для удобного доступа к выбранному городу - -#### Интеграция: - -- Селектор добавлен в `Layout` компонент -- Интегрирован в `StationCreatePage` и `CarrierCreatePage` -- Использует существующий `CityStore` для получения списка городов - -#### Файлы, которые были изменены: - -- `src/widgets/Layout/index.tsx` - добавлен CitySelector -- `src/pages/Station/StationCreatePage/index.tsx` - автоматическая подстановка города -- `src/pages/Carrier/CarrierCreatePage/index.tsx` - автоматическая подстановка города -- `src/shared/store/index.ts` - добавлен экспорт SelectedCityStore -- `src/widgets/index.ts` - добавлен экспорт CitySelector -- `src/shared/index.tsx` - добавлен экспорт hooks - -#### Новые файлы: - -- `src/shared/store/SelectedCityStore/index.ts` -- `src/widgets/CitySelector/index.tsx` -- `src/shared/hooks/useSelectedCity.ts` -- `src/shared/hooks/index.ts` - -### Использование: - -1. Пользователь выбирает город в селекторе в шапке приложения -2. Выбранный город сохраняется в localStorage -3. При создании новой станции или перевозчика выбранный город автоматически подставляется в форму -4. Пользователь может изменить город в форме если нужно - -Функциональность полностью интегрирована и готова к использованию. diff --git a/src/pages/MapPage/index.tsx b/src/pages/MapPage/index.tsx index 026e3b2..39ea4d6 100644 --- a/src/pages/MapPage/index.tsx +++ b/src/pages/MapPage/index.tsx @@ -52,7 +52,7 @@ import { FeatureLike } from "ol/Feature"; import { createEmpty, extend, getCenter } from "ol/extent"; // --- CUSTOM SCROLLBAR STYLES --- -const scrollbarHideStyles = ` +const scrollbarStyles = ` .scrollbar-hide { -ms-overflow-style: none; scrollbar-width: none; @@ -60,11 +60,34 @@ const scrollbarHideStyles = ` .scrollbar-hide::-webkit-scrollbar { display: none; } + + .scrollbar-visible { + scrollbar-width: thin; + scrollbar-color: #cbd5e1 #f1f5f9; + } + + .scrollbar-visible::-webkit-scrollbar { + width: 8px; + } + + .scrollbar-visible::-webkit-scrollbar-track { + background: #f1f5f9; + border-radius: 4px; + } + + .scrollbar-visible::-webkit-scrollbar-thumb { + background: #cbd5e1; + border-radius: 4px; + } + + .scrollbar-visible::-webkit-scrollbar-thumb:hover { + background: #94a3b8; + } `; if (typeof document !== "undefined") { const styleElement = document.createElement("style"); - styleElement.textContent = scrollbarHideStyles; + styleElement.textContent = scrollbarStyles; document.head.appendChild(styleElement); } @@ -73,7 +96,13 @@ if (typeof document !== "undefined") { import { languageInstance } from "@shared"; import { makeAutoObservable } from "mobx"; -import { stationsStore, routeStore, sightsStore, menuStore } from "@shared"; +import { + stationsStore, + routeStore, + sightsStore, + menuStore, + selectedCityStore, +} from "@shared"; // Функция для сброса кешей карты export const clearMapCaches = () => { @@ -101,6 +130,7 @@ interface ApiStation { name: string; latitude: number; longitude: number; + city_id: number; created_at?: string; } @@ -110,12 +140,12 @@ interface ApiSight { description: string; latitude: number; longitude: number; - created_at?: string; + city_id: number; + created_at?: string; } export type SortType = "name_asc" | "name_desc" | "date_asc" | "date_desc"; - class MapStore { constructor() { makeAutoObservable(this); @@ -151,16 +181,32 @@ class MapStore { return sorted.sort((a, b) => b.name.localeCompare(a.name)); case "date_asc": return sorted.sort((a, b) => { - if ('created_at' in a && 'created_at' in b && a.created_at && b.created_at) { - return new Date(a.created_at).getTime() - new Date(b.created_at).getTime(); + if ( + "created_at" in a && + "created_at" in b && + a.created_at && + b.created_at + ) { + return ( + new Date(a.created_at).getTime() - + new Date(b.created_at).getTime() + ); } // Фоллбэк: сортировка по ID, если дата недоступна return a.id - b.id; }); case "date_desc": return sorted.sort((a, b) => { - if ('created_at' in a && 'created_at' in b && a.created_at && b.created_at) { - return new Date(b.created_at).getTime() - new Date(a.created_at).getTime(); + if ( + "created_at" in a && + "created_at" in b && + a.created_at && + b.created_at + ) { + return ( + new Date(b.created_at).getTime() - + new Date(a.created_at).getTime() + ); } // Фоллбэк: сортировка по ID, если дата недоступна return b.id - a.id; @@ -178,7 +224,27 @@ class MapStore { get sortedSights(): ApiSight[] { return this.sortFeatures(this.sights, this.sightSort); } - + + // ГЕТТЕРЫ ДЛЯ ФИЛЬТРАЦИИ ПО ГОРОДУ + get filteredStations(): ApiStation[] { + const selectedCityId = selectedCityStore.selectedCityId; + if (!selectedCityId) { + return this.sortedStations; + } + return this.sortedStations.filter( + (station) => station.city_id === selectedCityId + ); + } + + get filteredSights(): ApiSight[] { + const selectedCityId = selectedCityStore.selectedCityId; + if (!selectedCityId) { + return this.sortedSights; + } + return this.sortedSights.filter( + (sight) => sight.city_id === selectedCityId + ); + } getRoutes = async () => { const response = await languageInstance("ru").get("/route"); @@ -243,7 +309,15 @@ class MapStore { address: "", system_name: name, }); - stationsStore.setCreateCommonData({ latitude, longitude, city_id: 1 }); + const selectedCityId = selectedCityStore.selectedCityId || 1; + const selectedCityName = + selectedCityStore.selectedCityName || "Неизвестный город"; + stationsStore.setCreateCommonData({ + latitude, + longitude, + city_id: selectedCityId, + city: selectedCityName, + }); await stationsStore.createStation(); createdItem = @@ -290,7 +364,11 @@ class MapStore { sightsStore.updateCreateSight("en", { name, address: "" }); sightsStore.updateCreateSight("zh", { name, address: "" }); - await sightsStore.createSightAction(1, { latitude, longitude }); + const selectedCityId = selectedCityStore.selectedCityId || 1; + await sightsStore.createSightAction(selectedCityId, { + latitude, + longitude, + }); createdItem = sightsStore.sights[sightsStore.sights.length - 1]; } else { throw new Error(`Unknown feature type for creation: ${featureType}`); @@ -1181,9 +1259,9 @@ class MapService { } public loadFeaturesFromApi( - apiStations: typeof mapStore.stations, + _apiStations: typeof mapStore.stations, apiRoutes: typeof mapStore.routes, - apiSights: typeof mapStore.sights + _apiSights: typeof mapStore.sights ): void { if (!this.map) return; @@ -1191,7 +1269,11 @@ class MapService { const pointFeatures: Feature[] = []; const lineFeatures: Feature[] = []; - apiStations.forEach((station) => { + // Используем фильтрованные данные из mapStore + const filteredStations = mapStore.filteredStations; + const filteredSights = mapStore.filteredSights; + + filteredStations.forEach((station) => { if (station.longitude == null || station.latitude == null) return; const point = new Point( transform( @@ -1206,7 +1288,7 @@ class MapService { pointFeatures.push(feature); }); - apiSights.forEach((sight) => { + filteredSights.forEach((sight) => { if (sight.longitude == null || sight.latitude == null) return; const point = new Point( transform([sight.longitude, sight.latitude], "EPSG:4326", projection) @@ -1329,6 +1411,7 @@ class MapService { 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(); @@ -1339,6 +1422,7 @@ class MapService { 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(); @@ -2176,7 +2260,7 @@ const MapControls: React.FC = ({ ); }; -import {observer} from "mobx-react-lite"; +import { observer } from "mobx-react-lite"; // --- MAP SIGHTBAR COMPONENT --- interface MapSightbarProps { @@ -2188,249 +2272,302 @@ interface MapSightbarProps { activeSection: string | null; setActiveSection: (section: string | null) => void; } -const MapSightbar: React.FC = observer(({ - mapService, - mapFeatures, - selectedFeature, - selectedIds, - setSelectedIds, - activeSection, - setActiveSection, -}) => { - const navigate = useNavigate(); - const [searchQuery, setSearchQuery] = useState(""); - const [stationSort, setStationSort] = useState("name_asc"); - const [sightSort, setSightSort] = useState("name_asc"); +const MapSightbar: React.FC = observer( + ({ + mapService, + mapFeatures, + selectedFeature, + selectedIds, + setSelectedIds, + activeSection, + setActiveSection, + }) => { + const navigate = useNavigate(); + const [searchQuery, setSearchQuery] = useState(""); + const [stationSort, setStationSort] = useState("name_asc"); + const [sightSort, setSightSort] = useState("name_asc"); - const { isOpen } = menuStore; - + const { isOpen } = menuStore; + const { selectedCityId } = selectedCityStore; - const actualFeatures = useMemo( - () => mapFeatures.filter((f) => !f.get("isProxy")), - [mapFeatures] - ); - - const filteredFeatures = useMemo(() => { - if (!searchQuery.trim()) return actualFeatures; - return actualFeatures.filter((f) => - ((f.get("name") as string) || "") - .toLowerCase() - .includes(searchQuery.toLowerCase()) + const actualFeatures = useMemo( + () => mapFeatures.filter((f) => !f.get("isProxy")), + [mapFeatures] ); - }, [actualFeatures, searchQuery]); - const handleFeatureClick = useCallback( - (id: string | number) => { - if (!mapService) return; - mapService.selectFeature(id); - }, - [mapService] - ); + // Создаем объединенный список всех объектов для поиска + const allFeatures = useMemo(() => { + const stations = mapStore.filteredStations.map((station) => { + const feature = new Feature({ + geometry: new Point( + transform( + [station.longitude, station.latitude], + "EPSG:4326", + "EPSG:3857" + ) + ), + name: station.name, + }); + feature.setId(`station-${station.id}`); + feature.set("featureType", "station"); + feature.set("created_at", station.created_at); + return feature; + }); - const handleDeleteFeature = useCallback( - (id: string | number, resource: string) => { - if (!mapService) return; - if (window.confirm("Вы действительно хотите удалить этот объект?")) { - mapService.deleteFeature(id, resource); + const sights = mapStore.filteredSights.map((sight) => { + const feature = new Feature({ + geometry: new Point( + transform( + [sight.longitude, sight.latitude], + "EPSG:4326", + "EPSG:3857" + ) + ), + name: sight.name, + description: sight.description, + }); + feature.setId(`sight-${sight.id}`); + feature.set("featureType", "sight"); + feature.set("created_at", sight.created_at); + return feature; + }); + + const lines = actualFeatures.filter( + (f) => f.get("featureType") === "route" + ); + + return [...stations, ...sights, ...lines]; + }, [ + mapStore.filteredStations, + mapStore.filteredSights, + actualFeatures, + selectedCityId, + mapStore, + ]); + + const filteredFeatures = useMemo(() => { + if (!searchQuery.trim()) return allFeatures; + return allFeatures.filter((f) => + ((f.get("name") as string) || "") + .toLowerCase() + .includes(searchQuery.toLowerCase()) + ); + }, [allFeatures, searchQuery]); + + const handleFeatureClick = useCallback( + (id: string | number) => { + if (!mapService) return; + mapService.selectFeature(id); + }, + [mapService] + ); + + const handleDeleteFeature = useCallback( + (id: string | number, resource: string) => { + if (!mapService) return; + if (window.confirm("Вы действительно хотите удалить этот объект?")) { + mapService.deleteFeature(id, resource); + } + }, + [mapService] + ); + + const handleCheckboxChange = useCallback( + (id: string | number) => { + if (!mapService) return; + const newSet = new Set(selectedIds); + if (newSet.has(id)) newSet.delete(id); + else newSet.add(id); + setSelectedIds(newSet); + mapService.setSelectedIds(newSet); + }, + [mapService, selectedIds, setSelectedIds] + ); + + const handleBulkDelete = useCallback(() => { + if (!mapService || selectedIds.size === 0) return; + if ( + window.confirm( + `Вы уверены, что хотите удалить ${selectedIds.size} объект(ов)?` + ) + ) { + mapService.deleteMultipleFeatures(Array.from(selectedIds)); + setSelectedIds(new Set()); } - }, - [mapService] - ); + }, [mapService, selectedIds, setSelectedIds]); - const handleCheckboxChange = useCallback( - (id: string | number) => { - if (!mapService) return; - const newSet = new Set(selectedIds); - if (newSet.has(id)) newSet.delete(id); - else newSet.add(id); - setSelectedIds(newSet); - mapService.setSelectedIds(newSet); - }, - [mapService, selectedIds, setSelectedIds] - ); + const handleEditFeature = useCallback( + (featureType: string, fullId: string | number) => { + const numericId = String(fullId).split("-")[1]; + if (!featureType || !numericId) return; + navigate(`/${featureType}/${numericId}/edit`); + }, + [navigate] + ); - const handleBulkDelete = useCallback(() => { - if (!mapService || selectedIds.size === 0) return; - if ( - window.confirm( - `Вы уверены, что хотите удалить ${selectedIds.size} объект(ов)?` - ) - ) { - mapService.deleteMultipleFeatures(Array.from(selectedIds)); - setSelectedIds(new Set()); - } - }, [mapService, selectedIds, setSelectedIds]); + const sortFeaturesByType = >( + features: T[], + sortType: SortType + ): T[] => { + const sorted = [...features]; + switch (sortType) { + case "name_asc": + return sorted.sort((a, b) => + ((a.get("name") as string) || "").localeCompare( + (b.get("name") as string) || "" + ) + ); + case "name_desc": + return sorted.sort((a, b) => + ((b.get("name") as string) || "").localeCompare( + (a.get("name") as string) || "" + ) + ); + case "date_asc": + return sorted.sort((a, b) => { + const aDate = a.get("created_at") + ? new Date(a.get("created_at")) + : new Date(0); + const bDate = b.get("created_at") + ? new Date(b.get("created_at")) + : new Date(0); + return aDate.getTime() - bDate.getTime(); + }); + case "date_desc": + return sorted.sort((a, b) => { + const aDate = a.get("created_at") + ? new Date(a.get("created_at")) + : new Date(0); + const bDate = b.get("created_at") + ? new Date(b.get("created_at")) + : new Date(0); + return bDate.getTime() - aDate.getTime(); + }); + default: + return sorted; + } + }; - const handleEditFeature = useCallback( - (featureType: string, fullId: string | number) => { - const numericId = String(fullId).split("-")[1]; - if (!featureType || !numericId) return; - navigate(`/${featureType}/${numericId}/edit`); - }, - [navigate] - ); + const stations = filteredFeatures.filter( + (f) => f.get("featureType") === "station" + ); + const lines = filteredFeatures.filter( + (f) => f.get("featureType") === "route" + ); + const sights = filteredFeatures.filter( + (f) => f.get("featureType") === "sight" + ); - const sortFeaturesByType = >( - features: T[], - sortType: SortType - ): T[] => { - const sorted = [...features]; - switch (sortType) { - case "name_asc": - return sorted.sort((a, b) => - ((a.get("name") as string) || "").localeCompare( - ((b.get("name") as string) || "") - ) - ); - case "name_desc": - return sorted.sort((a, b) => - ((b.get("name") as string) || "").localeCompare( - ((a.get("name") as string) || "") - ) - ); - case "date_asc": - return sorted.sort((a, b) => { - const aDate = a.get("created_at") - ? new Date(a.get("created_at")) - : new Date(0); - const bDate = b.get("created_at") - ? new Date(b.get("created_at")) - : new Date(0); - return aDate.getTime() - bDate.getTime(); - }); - case "date_desc": - return sorted.sort((a, b) => { - const aDate = a.get("created_at") - ? new Date(a.get("created_at")) - : new Date(0); - const bDate = b.get("created_at") - ? new Date(b.get("created_at")) - : new Date(0); - return bDate.getTime() - aDate.getTime(); - }); - default: - return sorted; - } - }; + const sortedStations = sortFeaturesByType(stations, stationSort); + const sortedSights = sortFeaturesByType(sights, sightSort); - const stations = filteredFeatures.filter( - (f) => f.get("featureType") === "station" - ); - const lines = filteredFeatures.filter((f) => f.get("featureType") === "route"); - const sights = filteredFeatures.filter( - (f) => f.get("featureType") === "sight" - ); + const renderFeatureList = ( + features: Feature[], + featureType: "station" | "route" | "sight", + IconComponent: React.ElementType + ) => ( +
+ {features.length > 0 ? ( + features.map((feature) => { + const fId = feature.getId(); + if (fId === undefined) return null; // TypeScript-safe + const fName = (feature.get("name") as string) || "Без названия"; + const isSelected = selectedFeature?.getId() === fId; + const isChecked = selectedIds.has(fId); - const sortedStations = sortFeaturesByType(stations, stationSort); - const sortedSights = sortFeaturesByType(sights, sightSort); - - const renderFeatureList = ( - features: Feature[], - featureType: "station" | "route" | "sight", - IconComponent: React.ElementType - ) => ( -
- {features.length > 0 ? ( - features.map((feature) => { - const fId = feature.getId(); - if (fId === undefined) return null; // TypeScript-safe - const fName = (feature.get("name") as string) || "Без названия"; - const isSelected = selectedFeature?.getId() === fId; - const isChecked = selectedIds.has(fId); - - return ( -
-
- handleCheckboxChange(fId)} - onClick={(e) => e.stopPropagation()} - aria-label={`Выбрать ${fName}`} - /> -
+ return (
handleFeatureClick(fId)} + key={String(fId)} + data-feature-id={fId} + className={`flex items-start p-2 rounded-md group transition-colors duration-150 ${ + isSelected + ? "bg-orange-100 border border-orange-300" + : "hover:bg-blue-50" + }`} > -
- )} - // @ts-ignore - size={16} +
+ handleCheckboxChange(fId)} + onClick={(e) => e.stopPropagation()} + aria-label={`Выбрать ${fName}`} /> - +
handleFeatureClick(fId)} + > +
+ )} + // @ts-ignore + size={16} + /> + + {fName} + +
+
+
+ +
-
- - -
-
- ); - }) - ) : ( -

Нет объектов этого типа.

- )} -
- ); + ); + }) + ) : ( +

Нет объектов этого типа.

+ )} +
+ ); - const toggleSection = (id: string) => - setActiveSection(activeSection === id ? null : id); + const toggleSection = (id: string) => + setActiveSection(activeSection === id ? null : id); - const sections = [ - { - id: "layers", - title: `Остановки (${sortedStations.length})`, - icon: , - count: sortedStations.length, - content: ( - <> -
- + const sections = [ + { + id: "layers", + title: `Остановки (${sortedStations.length})`, + icon: , + count: sortedStations.length, + sortControl: ( +
+
- {renderFeatureList(sortedStations, "station", MapPin)} - - ), - }, - { - id: "lines", - title: `Маршруты (${lines.length})`, - icon: , - count: lines.length, - content: renderFeatureList(lines, "route", ArrowRightLeft), - }, - { - id: "sights", - title: `Достопримечательности (${sortedSights.length})`, - icon: , - count: sortedSights.length, - content: ( - <> -
- + ), + content: renderFeatureList(sortedStations, "station", MapPin), + }, + { + id: "lines", + title: `Маршруты (${lines.length})`, + icon: , + count: lines.length, + sortControl: null, + content: renderFeatureList(lines, "route", ArrowRightLeft), + }, + { + id: "sights", + title: `Достопримечательности (${sortedSights.length})`, + icon: , + count: sortedSights.length, + sortControl: ( +
+
- {renderFeatureList(sortedSights, "sight", Landmark)} - - ), - }, - ]; + ), + content: renderFeatureList(sortedSights, "sight", Landmark), + }, + ]; - React.useEffect(() => { - console.log("isOpen changed:", isOpen); - }, [isOpen]); + React.useEffect(() => { + console.log("isOpen changed:", isOpen); + }, [isOpen]); - return ( -
-
-

Панель управления

-
-
- setSearchQuery(e.target.value)} - className="w-full px-3 py-2 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" - /> -
-
- {filteredFeatures.length === 0 && searchQuery ? ( -
- Ничего не найдено. -
- ) : ( - sections.map( - (s) => - (s.count > 0 || !searchQuery) && ( -
- -
-
{s.content}
+
+ + {s.icon} + + {s.title} +
+ + ▼ + + + {activeSection === s.id && ( + <> + {s.sortControl && ( +
{s.sortControl}
+ )} +
+
+ {s.content} +
+
+ + )}
-
- ) - ) + ) + ) + )} +
+ {selectedIds.size > 0 && ( +
+ +
)}
- {selectedIds.size > 0 && ( -
- -
- )} -
- ); -}); + ); + } +); // --- MAP PAGE COMPONENT --- -export const MapPage: React.FC = () => { +export const MapPage: React.FC = observer(() => { const mapRef = useRef(null); const tooltipRef = useRef(null); const [mapServiceInstance, setMapServiceInstance] = @@ -2584,6 +2729,8 @@ export const MapPage: React.FC = () => { string | null >(() => getStoredActiveSection() || "layers"); + const { selectedCityId } = selectedCityStore; + const handleFeaturesChange = useCallback( (feats: Feature[]) => setMapFeatures([...feats]), [] @@ -2750,6 +2897,22 @@ export const MapPage: React.FC = () => { saveActiveSection(activeSectionFromParent); }, [activeSectionFromParent]); + // Перезагружаем данные при изменении города + useEffect(() => { + if (mapServiceInstance && !isDataLoading) { + // Очищаем текущие объекты на карте + mapServiceInstance.pointSource.clear(); + mapServiceInstance.lineSource.clear(); + + // Загружаем новые данные с фильтрацией по городу + mapServiceInstance.loadFeaturesFromApi( + mapStore.stations, + mapStore.routes, + mapStore.sights + ); + } + }, [selectedCityId, mapServiceInstance, isDataLoading]); + const showLoader = isMapLoading || isDataLoading; const showContent = mapServiceInstance && !showLoader && !error; const isAnythingSelected = @@ -2866,4 +3029,4 @@ export const MapPage: React.FC = () => { )}
); -}; +}); diff --git a/src/widgets/SightTabs/CreateInformationTab/index.tsx b/src/widgets/SightTabs/CreateInformationTab/index.tsx index 119e70b..61cdc8f 100644 --- a/src/widgets/SightTabs/CreateInformationTab/index.tsx +++ b/src/widgets/SightTabs/CreateInformationTab/index.tsx @@ -514,4 +514,4 @@ export const CreateInformationTab = observer( ); } -); \ No newline at end of file +);