import {Coords, PlaceMarkProperties} from "../types.ts";

interface Properties extends PlaceMarkProperties{
  get(property: string): string
  set(property: string, value: string): void
}

export class PlaceMark{
  placemark: ymaps.Placemark;
  coords: Coords;
  properties: Properties;
  options: ymaps.IPlacemarkOptions;

  constructor(coords: Coords, properties: PlaceMarkProperties = {}, options: ymaps.IPlacemarkOptions = {}) {
    this.placemark = new window.ymaps.Placemark(coords, properties, options);
    this.coords = coords;

    this.properties = {
      ...properties,
      get: (property: string) => this.placemark.properties.get(property) as unknown as string,
      set: (property: string, value: string) => this.placemark.properties.set(property, value),
    };
    this.options = options;
  }

  addEventListener(eventName: string, callback: () => void) {
    this.placemark.events.add(eventName, callback);
  }
}import {Coords, PlaceMarkProperties} from "../types.ts";
import {PlaceMark} from "./PlaceMark.ts";
import {SatelliteMap} from "./SatelliteMap.ts";

class MapService {
  createMap(element: HTMLElement, options: {center?: Coords, zoom: number}){
    return new SatelliteMap(element, options);
  }

  createPlacemark(coords: Coords, properties: PlaceMarkProperties = {}, options: ymaps.IPlacemarkOptions = {}){
    return new PlaceMark(coords, properties, options);
  }
}

export default new MapService()import {Coords, MapEvent} from "../types.ts";
import {PlaceMark} from "./PlaceMark.ts";

export interface SatelliteMapOptions{
  center?: Coords;
  zoom: number;
}

export class SatelliteMap{
  yMap: ymaps.Map;

  constructor(container: HTMLElement, options: SatelliteMapOptions) {
    this.yMap = new window.ymaps.Map(container, options);
  }

  addEventListener(
    type: string,
    callback: (event: MapEvent) => void
  ){
    this.yMap.events.add(type, callback);
  }

  addPlaceMark(placeMark: PlaceMark) {
    this.yMap.geoObjects.add(placeMark.placemark);
  }

  removeAllPlaceMarks(){
    this.yMap.geoObjects.removeAll();
  }

  removePlaceMark(placeMark: PlaceMark){
    this.yMap.geoObjects.remove(placeMark.placemark);
  }

  destroy(){
    this.yMap.destroy();
  }
}import {createContext, FC, MutableRefObject, ReactNode, useContext, useEffect, useRef, useState} from 'react';
import {Coords, MapEvent} from "../types.ts";
import {PlaceMark} from "../services/PlaceMark.ts";
import {SatelliteMap} from "../services/SatelliteMap.ts";
import mapService from "../services/MapService.ts";

interface MapProviderProps{
  children: ReactNode;
}

interface MapContext{
  map: SatelliteMap | null;
  setMap: (map: SatelliteMap | null) => void;
  coords: Coords;
  setCoords: (coords: [number, number]) => void;
  currentMark: MutableRefObject<PlaceMark | null>;
  placemarks: PlaceMark[];
  addPlacemark: (placemarks: PlaceMark) => void;
  editingPlacemark: PlaceMark | null;
  setEditingPlacemark: (placemark: PlaceMark | null) => void;
}

const MapContext = createContext<MapContext | null>(null)

export const MapProvider: FC<MapProviderProps> = ({children}) => {
  const [map, setMap] = useState<SatelliteMap | null>(null);
  const currentMark = useRef<PlaceMark | null>(null);
  const [coords, setCoords] = useState<[number, number]>([0,0])
  const [placemarks, setPlacemarks] = useState<PlaceMark[]>([])
  const [editingPlacemark, setEditingPlacemark] = useState<PlaceMark | null>(null);

  const addPlacemark = (placemark: PlaceMark) => {
    placemark.addEventListener('click', () => {
      setEditingPlacemark(placemark);
    })

    map?.addPlaceMark(placemark);
    setPlacemarks(prev => [...prev, placemark])
  }

  const handleAddPlacemark = (event: MapEvent) => {
    if(!map) return

    const coords = event.get('coords');

    if (currentMark.current) {
      map.removePlaceMark(currentMark.current);
    }

    const newPlacemark = mapService.createPlacemark(coords, {
      hintContent: 'Метка',
      balloonContent: 'Кликнули на карту',
    });
    map.addPlaceMark(newPlacemark);
    currentMark.current = newPlacemark;
    setCoords(coords);
  }

  useEffect(() => {
    map?.addEventListener('click', handleAddPlacemark);
  }, [map]);

  return (
    <MapContext.Provider value={
      {coords, setCoords, placemarks, addPlacemark, editingPlacemark, setEditingPlacemark,
      map, setMap, currentMark}
    }>
      {children}
    </MapContext.Provider>
  );
};

export const useMapContext = () => {
  const context = useContext(MapContext);
  if (!context) {
    throw new Error("useMapContext must be used within a MapProvider");
  }
  return context;
};import {FC, useState} from "react";
import { useMapContext } from "../map-context";
import mapService from "../../services/MapService.ts";
import { Coords } from "../../types";
import style from './create-placemark.module.scss';
import EditPlacemark from "../edit-placemark";
import Input from "../ui/input";
import {useInput} from "../../hooks/useInput.ts";

const CreatePlacemarkControl: FC = () => {
  const { coords, setCoords, addPlacemark, editingPlacemark, setEditingPlacemark} = useMapContext();

  const [title, setTitle] = useState('');
  const [description, setDescription] = useState("");
  const [markplaceColor, setMarkplaceColor] = useState("islands#redIcon"); // Default icon type

  const handleInputTitle = useInput(setTitle);
  const handleInputDescription = useInput(setDescription);

  const handleCoordinateChange = (index: number, value: string) => {
    const newCoord = parseFloat(value);
    if (!isNaN(newCoord)) {
      const updatedCoords = [...coords] as Coords;
      updatedCoords[index] = newCoord;
      setCoords(updatedCoords);
    }
  };

  const handleInputX = (e: React.ChangeEvent<HTMLInputElement>) => {
    handleCoordinateChange(0, e.target.value);
  }

  const handleInputY = (e: React.ChangeEvent<HTMLInputElement>) => {
    handleCoordinateChange(1, e.target.value);
  }

  const handleAddPlacemark = () => {
    const newPlacemark = mapService.createPlacemark(coords, {
      hintContent: title,
      balloonContent: description,
      iconLayout: "default#image",
    }, {
      preset: markplaceColor,
    });
    addPlacemark(newPlacemark);
    setTitle("");
    setDescription("");
  };import { FC, useEffect, useState, ChangeEvent, FormEvent } from "react";
import { useMapContext } from "../map-context";
import Input from "../ui/input";
import { PlaceMark } from "../../services/PlaceMark";

interface EditPlacemarkProps {
  placemark: PlaceMark | null;
  onEdit: () => void;
}

const EditPlacemark: FC<EditPlacemarkProps> = ({ placemark, onEdit }) => {
  const { addPlacemark } = useMapContext();

  const [title, setTitle] = useState<string>("");
  const [description, setDescription] = useState<string>("");

  useEffect(() => {
    if (placemark) {
      const initialTitle = placemark.properties.get('hintContent') ?? '';
      const initialDescription = placemark.properties.get('balloonContent') ?? '';
      setTitle(initialTitle);
      setDescription(initialDescription);
    } else {
      setTitle("");
      setDescription("");
    }
  }, [placemark]);

  const handleTitleChange = (e: ChangeEvent<HTMLInputElement>) => {
    setTitle(e.target.value);
  };

  const handleDescriptionChange = (e: ChangeEvent<HTMLInputElement>) => {
    setDescription(e.target.value);
  };

  const handleSubmit = (e: FormEvent) => {
    e.preventDefault();
    if (!placemark) return;

    if (!title.trim()) {
      alert("Название не может быть пустым.");
      return;
    }

    placemark.properties.set("hintContent", title);
    placemark.properties.set("balloonContent", description);

    addPlacemark(placemark);
    onEdit();

    setTitle("");
    setDescription("");
  };

  const handleCancel = () => {
    onEdit();
    if (placemark) {
      setTitle(placemark.properties.get('hintContent') ?? '');
      setDescription(placemark.properties.get('balloonContent') ?? '');
    }
  };

  return (
    <div>
      <h2>Редактирование метки</h2>
      <form onSubmit={handleSubmit}>
        <div>
          <label htmlFor="placemark-title">Название:</label>
          <Input
            id="placemark-title"
            disabled={!placemark}
            type="text"
            value={title}
            onChange={handleTitleChange}
            required
          />
        </div>
        <div>
          <label htmlFor="placemark-description">Описание:</label>
          <Input
            id="placemark-description"
            disabled={!placemark}
            type="text"
            value={description}
            onChange={handleDescriptionChange}
          />
        </div>
        <div>
          <button type="submit" disabled={!placemark}>
            Сохранить
          </button>
          <button type="button" onClick={handleCancel} disabled={!placemark}>
            Отмена
          </button>
        </div>
      </form>
    </div>
  );
};

export default EditPlacemark;import { FC, useEffect, useRef } from "react";
import {Coords, PlaceMarkData} from "../../types.ts";
import { useMapContext } from "../map-context.tsx";
import {SatelliteMap} from "../../services/SatelliteMap.ts";

interface YandexMapProps {
  center: [number, number];
  zoom: number;
  marks: PlaceMarkData[];
}

const Map: FC<YandexMapProps> = ({ center, zoom }) => {
  const mapRef = useRef<HTMLDivElement>(null);
  const isMouseOverMap = useRef(false);

  const { placemarks, map, setMap, } = useMapContext();

  const handleMouseOverMap = () => isMouseOverMap.current = true;
  const handleMouseOutMap = () => isMouseOverMap.current = false;

  const createMap = (element: HTMLElement, options: {center?: Coords, zoom: number}) => {
    setMap(new SatelliteMap(element, options));
  }

  useEffect(() => {
    if (map || !window.ymaps) return;

    window.ymaps.ready(() => {
      createMap(mapRef.current!, {
        center,
        zoom,
      })
    });
  }, [center, zoom]);

  useEffect(() => {
    mapRef.current?.addEventListener('pointerenter', handleMouseOverMap);
    mapRef.current?.addEventListener('pointerleave', handleMouseOutMap);

    return () => {
      if (map) {
        placemarks.forEach(placemark => map.removePlaceMark(placemark));
        map.destroy();
        setMap(null);
      }

      mapRef.current?.removeEventListener('pointerenter', handleMouseOverMap);
      mapRef.current?.removeEventListener('pointerleave', handleMouseOutMap);
    };
  }, [map]);

  useEffect(() => {
    map?.removeAllPlaceMarks();
    placemarks.forEach(placemark => map?.addPlaceMark(placemark));
  }, [placemarks]);

  return (
    <>
      <div tabIndex={10} ref={mapRef} style={{ width: "45%", height: "80vh" }} />;
    </>
  );
};

export default Map;
 улучшить код