diff --git a/readme.md b/readme.md index 31bf033..94ca9e9 100644 --- a/readme.md +++ b/readme.md @@ -87,6 +87,85 @@ in other words : { id: string, plateNumber: number, name: string, path: string }[] ``` +### County Map Feature + +When enabled, clicking on a city displays a popup showing the county (ilçe) map of that city. This feature: +- Must be explicitly enabled via `showCountyMapOnClick` prop +- Requires county data to be imported separately to keep bundle size small when not used +- Can be customized via `countyMapWrapper` prop +- Supports all the same interaction handlers as the main map + +#### Basic Usage + +```javascript +import TurkeyMap from 'turkey-map-react'; +import { istanbulCounties, ankaraCounties } from 'turkey-map-react/lib/data/counties'; + +// Import only the county data you need +const countyData = { + istanbul: istanbulCounties, + ankara: ankaraCounties +}; + + +``` + +#### Handling County Events + +```javascript + { + console.log(`${county.name} in ${city.name} was clicked!`); + }} +/> +``` + +#### Custom County Map Wrapper + +```javascript +const customCountyMapWrapper = (countyMapPopup, city, countyData) => ( +
+

Custom Header for {city.name}

+ {countyMapPopup} +
+); + + +``` + +#### Providing Custom County Data + +County data must follow the `CountyData` type structure: + +```javascript +import { CountyData } from 'turkey-map-react'; + +const myCustomCountyData: CountyData = { + cityId: "mycity", + cityName: "My City", + counties: [ + { id: "county1", name: "County 1", path: "M 0 0 L 100 0..." }, + { id: "county2", name: "County 2", path: "M 100 0 L 200 0..." } + ] +}; + + +``` + +**Note on Sample Data:** The built-in county data for Istanbul, Ankara, and Izmir uses placeholder rectangular paths for demonstration purposes only. These are NOT actual geographic boundaries. For production use, you should provide real geographic SVG data for county boundaries. Consider using geographic data sources like GADM or OpenStreetMap for accurate county shapes. + ## API ### Types @@ -94,6 +173,8 @@ in other words : | Type | Description | | :---------------- | :-------------------------------------------------------------------------------------- | | *CityType* | { **id**: *string*, **plateNumber**: *number*, **name**: *string*, **path**: *string* } | +| *CountyType* | { **id**: *string*, **name**: *string*, **path**: *string* } | +| *CountyData* | { **cityId**: *string*, **cityName**: *string*, **counties**: *CountyType*[] } | | *ViewBoxType* | { **top**: *number*, **left**: *number*, **width**: *number*, **height**: *number* } | | *CustomStyleType* | { **idleColor**: *string*, **hoverColor**: *string* } | @@ -111,6 +192,10 @@ in other words : | cityWrapper | City DOMs are wrapped by provided component. | ( **cityComponent**: *JSX.Element*, **city** : *CityType* ) => *JSX.Element* | *Unwrapped city* | 1.0.0 | | onHover | Event when a city hovered on the map. | ( **city** : *CityType* ) => *void* | - | 1.0.0 | | onClick | Event when a city clicked on the map | ( **city** : *CityType* ) => *void* | - | 1.0.0 | +| showCountyMapOnClick | Enables county map popup when a city is clicked | *boolean* | *false* | 3.0.0 | +| countyData | County data for cities (import only what you need) | *Record* | *undefined* | 3.0.0 | +| countyMapWrapper | Custom wrapper for county map popup | ( **popup**: *JSX.Element*, **city**: *CityType*, **data**: *CountyData* ) => *JSX.Element* | *Default popup* | 3.0.0 | +| onCountyClick | Event when a county is clicked in the county map | ( **county**: *CountyType*, **city**: *CityType* ) => *void* | - | 3.0.0 | ### Styling diff --git a/src/CountyMapPopup.tsx b/src/CountyMapPopup.tsx new file mode 100644 index 0000000..e7fa7b3 --- /dev/null +++ b/src/CountyMapPopup.tsx @@ -0,0 +1,255 @@ +import React, { MouseEventHandler, useCallback, useState } from 'react'; +import { Property } from 'csstype'; + +export type CountyType = { + id: string; + name: string; + path: string; +}; + +export type CountyData = { + cityId: string; + cityName: string; + counties: CountyType[]; +}; + +interface ICountyMapPopupProps { + countyData: CountyData; + onClose: () => void; + customStyle?: { idleColor: string, hoverColor: string }; + hoverable?: boolean; + showTooltip?: boolean; + tooltipText?: string; + onCountyHover?: (county: CountyType) => void; + onCountyClick?: (county: CountyType) => void; +} + +const CountyMapPopup: React.FC = ({ + countyData, + onClose, + customStyle = { idleColor: "#444", hoverColor: "#dc3522" }, + hoverable = true, + showTooltip = false, + tooltipText, + onCountyHover, + onCountyClick +}) => { + const [hoveredCountyName, setHoveredCountyName] = useState(undefined); + const [tooltipStyle, setTooltipStyle] = useState<{ + left: number, + top: number, + visibility?: Property.Visibility, + animation?: Property.Animation + }>({ left: 0, top: 0, visibility: "hidden" }); + + // Handle ESC key to close popup + React.useEffect(() => { + const handleKeyDown = (event: KeyboardEvent) => { + if (event.key === 'Escape') { + onClose(); + } + }; + + document.addEventListener('keydown', handleKeyDown); + return () => { + document.removeEventListener('keydown', handleKeyDown); + }; + }, [onClose]); + + const handleMouseEvent = useCallback(( + event: React.MouseEvent, + callback: (county: CountyType) => void + ) => { + const element = event.target as Element; + + if (element.tagName === 'path') { + const parent = element.parentNode as Element; + + const countyId = parent.getAttribute('id') ?? ""; + const countyPath = element.getAttribute("d") ?? ""; + const countyName: string = parent.getAttribute('data-ilceadi') ?? ""; + const county: CountyType = { id: countyId, name: countyName, path: countyPath }; + + if (callback && typeof callback === 'function') { + callback(county); + } + } + }, []); + + const handleHover = useCallback((county: CountyType) => { + setHoveredCountyName(county.name); + if (onCountyHover) { + onCountyHover(county); + } + }, [onCountyHover]); + + const handleOnHover = useCallback((event: React.MouseEvent): void => { + handleMouseEvent(event, handleHover); + }, [handleMouseEvent, handleHover]); + + const handleOnClick = useCallback((event: React.MouseEvent): void => { + if (onCountyClick) { + handleMouseEvent(event, onCountyClick); + } + }, [onCountyClick, handleMouseEvent]); + + const handleOnMouseMove: MouseEventHandler = useCallback((event) => { + setTooltipStyle(prevState => ({ + ...prevState, + left: event.pageX + 16, + top: event.pageY - 32 + })); + }, []); + + const handleOnMouseEnter = useCallback((event: React.MouseEvent): void => { + const target = event.currentTarget; + const path = target.querySelector('path'); + if (path) { + path.style.fill = customStyle.hoverColor; + } + if (!showTooltip) return; + + setTooltipStyle(prevState => ({ + ...prevState, + animation: undefined, + visibility: "visible" + })); + }, [customStyle.hoverColor, showTooltip]); + + const handleOnMouseLeave = useCallback((event: React.MouseEvent): void => { + const target = event.currentTarget; + const path = target.querySelector('path'); + if (path) { + path.style.fill = customStyle.idleColor; + } + if (!showTooltip) return; + + setTooltipStyle(prevState => ({ + ...prevState, + visibility: undefined, + animation: `0.1s county_react_map_tooltip_fade_out forwards ease-out`, + })); + }, [customStyle.idleColor, showTooltip]); + + const handleOverlayClick = useCallback((event: React.MouseEvent) => { + if (event.target === event.currentTarget) { + onClose(); + } + }, [onClose]); + + return ( +
+
+
+

{countyData.cityName} - İlçeler

+ +
+ +
+ {showTooltip && ( +
+ {tooltipText || hoveredCountyName} +
+ )} + + + + {countyData.counties.map((county) => ( + hoverable ? handleOnHover(event) : undefined} + onMouseMove={showTooltip ? handleOnMouseMove : undefined} + onClick={handleOnClick} + > + + + ))} + + + + {showTooltip && } +
+
+
+ ); +}; + +export default CountyMapPopup; diff --git a/src/__tests__/CountyMapPopup.test.tsx b/src/__tests__/CountyMapPopup.test.tsx new file mode 100644 index 0000000..15d4d08 --- /dev/null +++ b/src/__tests__/CountyMapPopup.test.tsx @@ -0,0 +1,211 @@ +import React from 'react'; +import { render, screen, fireEvent } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import CountyMapPopup, { CountyData } from '../CountyMapPopup'; + +describe('CountyMapPopup Component', () => { + const mockCountyData: CountyData = { + cityId: "test-city", + cityName: "Test City", + counties: [ + { id: "county1", name: "County 1", path: "M 0 0 L 100 0 L 100 100 L 0 100 Z" }, + { id: "county2", name: "County 2", path: "M 100 0 L 200 0 L 200 100 L 100 100 Z" } + ] + }; + + const mockOnClose = jest.fn(); + + beforeEach(() => { + mockOnClose.mockClear(); + }); + + describe('Basic Rendering', () => { + it('should render without crashing', () => { + const { container } = render( + + ); + + expect(container.querySelector('#county-map-container')).toBeInTheDocument(); + }); + + it('should display city name in header', () => { + render(); + + expect(screen.getByText(/Test City - İlçeler/i)).toBeInTheDocument(); + }); + + it('should render all counties', () => { + const { container } = render( + + ); + + const countyGroups = container.querySelectorAll('g[data-ilceadi]'); + expect(countyGroups.length).toBe(2); + }); + + it('should have close button', () => { + render(); + + const closeButton = screen.getByLabelText('Close county map'); + expect(closeButton).toBeInTheDocument(); + }); + + it('should have proper accessibility attributes', () => { + const { container } = render( + + ); + + const dialog = container.querySelector('[role="dialog"]'); + expect(dialog).toBeInTheDocument(); + expect(dialog).toHaveAttribute('aria-modal', 'true'); + expect(dialog).toHaveAttribute('aria-labelledby', 'county-map-title'); + + const title = container.querySelector('#county-map-title'); + expect(title).toBeInTheDocument(); + }); + }); + + describe('Interaction', () => { + it('should call onClose when close button is clicked', () => { + render(); + + const closeButton = screen.getByLabelText('Close county map'); + fireEvent.click(closeButton); + + expect(mockOnClose).toHaveBeenCalledTimes(1); + }); + + it('should call onClose when ESC key is pressed', () => { + render(); + + fireEvent.keyDown(document, { key: 'Escape' }); + + expect(mockOnClose).toHaveBeenCalledTimes(1); + }); + + it('should call onClose when overlay is clicked', () => { + const { container } = render( + + ); + + const overlay = container.firstChild as HTMLElement; + fireEvent.click(overlay); + + expect(mockOnClose).toHaveBeenCalledTimes(1); + }); + + it('should not call onClose when popup content is clicked', () => { + const { container } = render( + + ); + + const popupContent = container.querySelector('#county-map-container') as HTMLElement; + fireEvent.click(popupContent); + + expect(mockOnClose).not.toHaveBeenCalled(); + }); + + it('should call onCountyClick when a county is clicked', () => { + const onCountyClick = jest.fn(); + const { container } = render( + + ); + + const firstPath = container.querySelector('path'); + if (firstPath) { + fireEvent.click(firstPath); + expect(onCountyClick).toHaveBeenCalled(); + const countyArg = onCountyClick.mock.calls[0][0]; + expect(countyArg).toHaveProperty('id'); + expect(countyArg).toHaveProperty('name'); + expect(countyArg).toHaveProperty('path'); + } + }); + + it('should call onCountyHover when hovering over a county', () => { + const onCountyHover = jest.fn(); + const { container } = render( + + ); + + const firstPath = container.querySelector('path'); + if (firstPath && firstPath.parentElement) { + fireEvent.mouseOver(firstPath); + expect(onCountyHover).toHaveBeenCalled(); + } + }); + }); + + describe('Styling', () => { + it('should apply custom idle color', () => { + const customStyle = { idleColor: '#ff0000', hoverColor: '#00ff00' }; + const { container } = render( + + ); + + const firstPath = container.querySelector('path'); + expect(firstPath).toHaveStyle({ fill: '#ff0000' }); + }); + + it('should change fill color on mouse enter', () => { + const customStyle = { idleColor: '#ff0000', hoverColor: '#00ff00' }; + const { container } = render( + + ); + + const firstCountyGroup = container.querySelector('g[data-ilceadi]'); + const firstPath = container.querySelector('path') as SVGPathElement; + + if (firstCountyGroup && firstPath) { + expect(firstPath.style.fill).toBe('#ff0000'); + fireEvent.mouseEnter(firstCountyGroup); + expect(firstPath.style.fill).toBe('#00ff00'); + } + }); + }); + + describe('Tooltip Functionality', () => { + it('should render tooltip when showTooltip is true', () => { + const { container } = render( + + ); + + const tooltip = container.querySelector('#county-map-tooltip'); + expect(tooltip).toBeInTheDocument(); + }); + + it('should not render tooltip when showTooltip is false', () => { + const { container } = render( + + ); + + const tooltip = container.querySelector('#county-map-tooltip'); + expect(tooltip).not.toBeInTheDocument(); + }); + }); +}); diff --git a/src/__tests__/TurkeyMap.test.tsx b/src/__tests__/TurkeyMap.test.tsx index e0e1b6b..f312f30 100644 --- a/src/__tests__/TurkeyMap.test.tsx +++ b/src/__tests__/TurkeyMap.test.tsx @@ -212,4 +212,200 @@ describe('TurkeyMap Component', () => { expect(initialCityCount).toBe(afterRerenderCount); }); }); + + describe('County Map Functionality', () => { + const mockCountyData = { + istanbul: { + cityId: "istanbul", + cityName: "İstanbul", + counties: [ + { id: "kadikoy", name: "Kadıköy", path: "M 0 0 L 100 0 L 100 100 L 0 100 Z" }, + { id: "besiktas", name: "Beşiktaş", path: "M 100 0 L 200 0 L 200 100 L 100 100 Z" } + ] + } + }; + + it('should not show county map by default', () => { + const { container } = render(); + const countyMap = container.querySelector('#county-map-container'); + expect(countyMap).not.toBeInTheDocument(); + }); + + it('should not show county map when showCountyMapOnClick is false', () => { + const { container } = render( + + ); + + const firstPath = container.querySelector('path'); + if (firstPath) { + fireEvent.click(firstPath); + const countyMap = container.querySelector('#county-map-container'); + expect(countyMap).not.toBeInTheDocument(); + } + }); + + it('should show county map when city is clicked and showCountyMapOnClick is true', () => { + const customData = { + cities: [ + { id: 'istanbul', plateNumber: 34, name: 'İstanbul', path: 'M 0 0 L 100 100' } + ] + }; + + const { container } = render( + + ); + + const firstPath = container.querySelector('path'); + if (firstPath) { + fireEvent.click(firstPath); + const countyMap = container.querySelector('#county-map-container'); + expect(countyMap).toBeInTheDocument(); + } + }); + + it('should not show county map if county data is not available for the city', () => { + const customData = { + cities: [ + { id: 'ankara', plateNumber: 6, name: 'Ankara', path: 'M 0 0 L 100 100' } + ] + }; + + const { container } = render( + + ); + + const firstPath = container.querySelector('path'); + if (firstPath) { + fireEvent.click(firstPath); + const countyMap = container.querySelector('#county-map-container'); + expect(countyMap).not.toBeInTheDocument(); + } + }); + + it('should close county map when close button is clicked', () => { + const customData = { + cities: [ + { id: 'istanbul', plateNumber: 34, name: 'İstanbul', path: 'M 0 0 L 100 100' } + ] + }; + + const { container } = render( + + ); + + const firstPath = container.querySelector('path'); + if (firstPath) { + fireEvent.click(firstPath); + + const closeButton = screen.getByLabelText('Close county map'); + expect(closeButton).toBeInTheDocument(); + + fireEvent.click(closeButton); + const countyMap = container.querySelector('#county-map-container'); + expect(countyMap).not.toBeInTheDocument(); + } + }); + + it('should call onClick handler even when showCountyMapOnClick is true', () => { + const onClickMock = jest.fn(); + const customData = { + cities: [ + { id: 'istanbul', plateNumber: 34, name: 'İstanbul', path: 'M 0 0 L 100 100' } + ] + }; + + const { container } = render( + + ); + + const firstPath = container.querySelector('path'); + if (firstPath) { + fireEvent.click(firstPath); + expect(onClickMock).toHaveBeenCalled(); + } + }); + + it('should call onCountyClick when a county is clicked', () => { + const onCountyClick = jest.fn(); + const customData = { + cities: [ + { id: 'istanbul', plateNumber: 34, name: 'İstanbul', path: 'M 0 0 L 100 100' } + ] + }; + + const { container } = render( + + ); + + // Open county map + const cityPath = container.querySelector('path'); + if (cityPath) { + fireEvent.click(cityPath); + + // Click on a county + const countyPath = container.querySelectorAll('path')[1]; // Get county path + if (countyPath) { + fireEvent.click(countyPath); + expect(onCountyClick).toHaveBeenCalled(); + } + } + }); + + it('should apply custom wrapper to county map', () => { + const customData = { + cities: [ + { id: 'istanbul', plateNumber: 34, name: 'İstanbul', path: 'M 0 0 L 100 100' } + ] + }; + + const countyMapWrapper = (countyMapPopup: React.ReactElement) => { + return ( +
+ {countyMapPopup} +
+ ); + }; + + const { container } = render( + + ); + + const firstPath = container.querySelector('path'); + if (firstPath) { + fireEvent.click(firstPath); + const customCountyMap = container.querySelector('[data-testid="custom-county-map"]'); + expect(customCountyMap).toBeInTheDocument(); + } + }); + }); }); diff --git a/src/data/counties/ankara.ts b/src/data/counties/ankara.ts new file mode 100644 index 0000000..ec575e7 --- /dev/null +++ b/src/data/counties/ankara.ts @@ -0,0 +1,139 @@ +import { CountyData } from '../../CountyMapPopup'; + +/** + * Sample county data for Ankara + * + * NOTE: The SVG path data uses placeholder rectangles for demonstration. + * For production, replace with actual geographic county boundaries. + */ +export const ankaraCounties: CountyData = { + cityId: "ankara", + cityName: "Ankara", + counties: [ + { + id: "akyurt", + name: "Akyurt", + path: "M 700,400 L 780,400 L 780,480 L 700,480 Z" + }, + { + id: "altindag", + name: "Altındağ", + path: "M 480,380 L 560,380 L 560,460 L 480,460 Z" + }, + { + id: "ayas", + name: "Ayaş", + path: "M 150,350 L 250,350 L 250,450 L 150,450 Z" + }, + { + id: "bala", + name: "Bala", + path: "M 700,500 L 800,500 L 800,600 L 700,600 Z" + }, + { + id: "beypazari", + name: "Beypazarı", + path: "M 200,250 L 300,250 L 300,350 L 200,350 Z" + }, + { + id: "camlidere", + name: "Çamlıdere", + path: "M 250,150 L 350,150 L 350,250 L 250,250 Z" + }, + { + id: "cankaya", + name: "Çankaya", + path: "M 500,450 L 580,450 L 580,530 L 500,530 Z" + }, + { + id: "cubuk", + name: "Çubuk", + path: "M 550,250 L 650,250 L 650,350 L 550,350 Z" + }, + { + id: "elmadag", + name: "Elmadağ", + path: "M 650,300 L 730,300 L 730,380 L 650,380 Z" + }, + { + id: "etimesgut", + name: "Etimesgut", + path: "M 380,400 L 460,400 L 460,480 L 380,480 Z" + }, + { + id: "evren", + name: "Evren", + path: "M 600,600 L 680,600 L 680,680 L 600,680 Z" + }, + { + id: "golbasi", + name: "Gölbaşı", + path: "M 550,550 L 640,550 L 640,640 L 550,640 Z" + }, + { + id: "gudul", + name: "Güdül", + path: "M 300,300 L 380,300 L 380,380 L 300,380 Z" + }, + { + id: "haymana", + name: "Haymana", + path: "M 400,600 L 500,600 L 500,700 L 400,700 Z" + }, + { + id: "kahramankazan", + name: "Kahramankazan", + path: "M 400,300 L 480,300 L 480,380 L 400,380 Z" + }, + { + id: "kalecik", + name: "Kalecik", + path: "M 700,250 L 780,250 L 780,330 L 700,330 Z" + }, + { + id: "kecioren", + name: "Keçiören", + path: "M 480,320 L 560,320 L 560,400 L 480,400 Z" + }, + { + id: "kizilcahamam", + name: "Kızılcahamam", + path: "M 400,150 L 500,150 L 500,250 L 400,250 Z" + }, + { + id: "mamak", + name: "Mamak", + path: "M 560,400 L 640,400 L 640,480 L 560,480 Z" + }, + { + id: "nallihan", + name: "Nallıhan", + path: "M 150,200 L 250,200 L 250,300 L 150,300 Z" + }, + { + id: "polatli", + name: "Polatlı", + path: "M 200,500 L 320,500 L 320,620 L 200,620 Z" + }, + { + id: "pursaklar", + name: "Pursaklar", + path: "M 450,350 L 520,350 L 520,420 L 450,420 Z" + }, + { + id: "sincan", + name: "Sincan", + path: "M 320,400 L 400,400 L 400,480 L 320,480 Z" + }, + { + id: "sereflikochisar", + name: "Şereflikoçhisar", + path: "M 500,650 L 600,650 L 600,750 L 500,750 Z" + }, + { + id: "yenimahalle", + name: "Yenimahalle", + path: "M 420,350 L 500,350 L 500,430 L 420,430 Z" + } + ] +}; diff --git a/src/data/counties/index.ts b/src/data/counties/index.ts new file mode 100644 index 0000000..d1792f6 --- /dev/null +++ b/src/data/counties/index.ts @@ -0,0 +1,10 @@ +// County data exports for individual cities +// Import only the county data you need to keep bundle size small +// Example: import { istanbulCounties } from 'turkey-map-react/lib/data/counties'; + +export { istanbulCounties } from './istanbul'; +export { ankaraCounties } from './ankara'; +export { izmirCounties } from './izmir'; + +// Re-export CountyData type for convenience +export type { CountyData, CountyType } from '../../CountyMapPopup'; diff --git a/src/data/counties/istanbul.ts b/src/data/counties/istanbul.ts new file mode 100644 index 0000000..b2d3cc3 --- /dev/null +++ b/src/data/counties/istanbul.ts @@ -0,0 +1,217 @@ +import { CountyData } from '../../CountyMapPopup'; + +/** + * Sample county data for Istanbul + * + * NOTE: The SVG path data in this file uses placeholder rectangles for demonstration purposes. + * These are NOT actual geographic boundaries of Istanbul's counties. + * + * For production use, you should: + * 1. Replace these paths with actual geographic SVG data + * 2. Use real county boundary coordinates + * 3. Consider using a geographic data source like GADM or OpenStreetMap + * + * The current rectangular paths are provided as examples to show the data structure + * and to allow testing of the county map functionality. + */ +export const istanbulCounties: CountyData = { + cityId: "istanbul", + cityName: "İstanbul", + counties: [ + { + id: "adalar", + name: "Adalar", + path: "M 850,450 L 900,450 L 900,500 L 850,500 Z" + }, + { + id: "arnavutkoy", + name: "Arnavutköy", + path: "M 200,150 L 280,150 L 280,220 L 200,220 Z" + }, + { + id: "atasehir", + name: "Ataşehir", + path: "M 650,400 L 720,400 L 720,460 L 650,460 Z" + }, + { + id: "avcilar", + name: "Avcılar", + path: "M 350,450 L 420,450 L 420,520 L 350,520 Z" + }, + { + id: "bagcilar", + name: "Bağcılar", + path: "M 420,400 L 490,400 L 490,460 L 420,460 Z" + }, + { + id: "bahcelievler", + name: "Bahçelievler", + path: "M 450,450 L 520,450 L 520,520 L 450,520 Z" + }, + { + id: "bakirkoy", + name: "Bakırköy", + path: "M 400,500 L 470,500 L 470,570 L 400,570 Z" + }, + { + id: "basaksehir", + name: "Başakşehir", + path: "M 350,350 L 430,350 L 430,420 L 350,420 Z" + }, + { + id: "bayrampasa", + name: "Bayrampaşa", + path: "M 470,400 L 530,400 L 530,450 L 470,450 Z" + }, + { + id: "besiktas", + name: "Beşiktaş", + path: "M 520,300 L 580,300 L 580,360 L 520,360 Z" + }, + { + id: "beykoz", + name: "Beykoz", + path: "M 650,200 L 750,200 L 750,300 L 650,300 Z" + }, + { + id: "beylikduzu", + name: "Beylikdüzü", + path: "M 280,500 L 360,500 L 360,570 L 280,570 Z" + }, + { + id: "beyoglu", + name: "Beyoğlu", + path: "M 540,320 L 600,320 L 600,380 L 540,380 Z" + }, + { + id: "buyukcekmece", + name: "Büyükçekmece", + path: "M 200,450 L 290,450 L 290,540 L 200,540 Z" + }, + { + id: "catalca", + name: "Çatalca", + path: "M 100,250 L 210,250 L 210,360 L 100,360 Z" + }, + { + id: "cekmekoy", + name: "Çekmeköy", + path: "M 700,300 L 770,300 L 770,370 L 700,370 Z" + }, + { + id: "esenler", + name: "Esenler", + path: "M 440,420 L 500,420 L 500,470 L 440,470 Z" + }, + { + id: "esenyurt", + name: "Esenyurt", + path: "M 300,400 L 380,400 L 380,480 L 300,480 Z" + }, + { + id: "eyupsultan", + name: "Eyüpsultan", + path: "M 480,280 L 560,280 L 560,350 L 480,350 Z" + }, + { + id: "fatih", + name: "Fatih", + path: "M 520,360 L 580,360 L 580,420 L 520,420 Z" + }, + { + id: "gaziosmanpasa", + name: "Gaziosmanpaşa", + path: "M 450,320 L 520,320 L 520,380 L 450,380 Z" + }, + { + id: "gungoren", + name: "Güngören", + path: "M 430,450 L 480,450 L 480,500 L 430,500 Z" + }, + { + id: "kadikoy", + name: "Kadıköy", + path: "M 620,420 L 690,420 L 690,490 L 620,490 Z" + }, + { + id: "kagithane", + name: "Kâğıthane", + path: "M 500,320 L 560,320 L 560,370 L 500,370 Z" + }, + { + id: "kartal", + name: "Kartal", + path: "M 700,480 L 780,480 L 780,560 L 700,560 Z" + }, + { + id: "kucukcekmece", + name: "Küçükçekmece", + path: "M 360,450 L 440,450 L 440,530 L 360,530 Z" + }, + { + id: "maltepe", + name: "Maltepe", + path: "M 680,450 L 750,450 L 750,520 L 680,520 Z" + }, + { + id: "pendik", + name: "Pendik", + path: "M 750,500 L 830,500 L 830,580 L 750,580 Z" + }, + { + id: "sancaktepe", + name: "Sancaktepe", + path: "M 730,370 L 800,370 L 800,440 L 730,440 Z" + }, + { + id: "sariyer", + name: "Sarıyer", + path: "M 500,180 L 600,180 L 600,280 L 500,280 Z" + }, + { + id: "silivri", + name: "Silivri", + path: "M 100,450 L 210,450 L 210,560 L 100,560 Z" + }, + { + id: "sultanbeyli", + name: "Sultanbeyli", + path: "M 780,400 L 850,400 L 850,470 L 780,470 Z" + }, + { + id: "sultangazi", + name: "Sultangazi", + path: "M 420,320 L 490,320 L 490,380 L 420,380 Z" + }, + { + id: "sile", + name: "Şile", + path: "M 800,150 L 900,150 L 900,250 L 800,250 Z" + }, + { + id: "sisli", + name: "Şişli", + path: "M 530,330 L 590,330 L 590,390 L 530,390 Z" + }, + { + id: "tuzla", + name: "Tuzla", + path: "M 800,500 L 880,500 L 880,580 L 800,580 Z" + }, + { + id: "umraniye", + name: "Ümraniye", + path: "M 680,360 L 750,360 L 750,430 L 680,430 Z" + }, + { + id: "uskudar", + name: "Üsküdar", + path: "M 600,380 L 670,380 L 670,450 L 600,450 Z" + }, + { + id: "zeytinburnu", + name: "Zeytinburnu", + path: "M 480,450 L 540,450 L 540,510 L 480,510 Z" + } + ] +}; diff --git a/src/data/counties/izmir.ts b/src/data/counties/izmir.ts new file mode 100644 index 0000000..35290a5 --- /dev/null +++ b/src/data/counties/izmir.ts @@ -0,0 +1,159 @@ +import { CountyData } from '../../CountyMapPopup'; + +/** + * Sample county data for Izmir + * + * NOTE: The SVG path data uses placeholder rectangles for demonstration. + * For production, replace with actual geographic county boundaries. + */ +export const izmirCounties: CountyData = { + cityId: "izmir", + cityName: "İzmir", + counties: [ + { + id: "aliaga", + name: "Aliağa", + path: "M 300,150 L 380,150 L 380,230 L 300,230 Z" + }, + { + id: "balcova", + name: "Balçova", + path: "M 380,480 L 450,480 L 450,550 L 380,550 Z" + }, + { + id: "bayindir", + name: "Bayındır", + path: "M 650,550 L 730,550 L 730,630 L 650,630 Z" + }, + { + id: "bayrakli", + name: "Bayraklı", + path: "M 420,380 L 490,380 L 490,450 L 420,450 Z" + }, + { + id: "bergama", + name: "Bergama", + path: "M 350,100 L 470,100 L 470,220 L 350,220 Z" + }, + { + id: "bornova", + name: "Bornova", + path: "M 480,420 L 560,420 L 560,500 L 480,500 Z" + }, + { + id: "buca", + name: "Buca", + path: "M 520,480 L 600,480 L 600,560 L 520,560 Z" + }, + { + id: "cesme", + name: "Çeşme", + path: "M 100,500 L 180,500 L 180,580 L 100,580 Z" + }, + { + id: "cigli", + name: "Çiğli", + path: "M 380,320 L 460,320 L 460,400 L 380,400 Z" + }, + { + id: "dikili", + name: "Dikili", + path: "M 250,200 L 330,200 L 330,280 L 250,280 Z" + }, + { + id: "foca", + name: "Foça", + path: "M 300,250 L 380,250 L 380,330 L 300,330 Z" + }, + { + id: "gaziemir", + name: "Gaziemir", + path: "M 450,520 L 520,520 L 520,590 L 450,590 Z" + }, + { + id: "guzelbahce", + name: "Güzelbahçe", + path: "M 320,520 L 390,520 L 390,590 L 320,590 Z" + }, + { + id: "karabaglar", + name: "Karabağlar", + path: "M 440,460 L 510,460 L 510,530 L 440,530 Z" + }, + { + id: "karaburun", + name: "Karaburun", + path: "M 150,400 L 240,400 L 240,490 L 150,490 Z" + }, + { + id: "karsiyaka", + name: "Karşıyaka", + path: "M 400,360 L 470,360 L 470,430 L 400,430 Z" + }, + { + id: "kemalpasa", + name: "Kemalpaşa", + path: "M 550,350 L 640,350 L 640,440 L 550,440 Z" + }, + { + id: "kinik", + name: "Kınık", + path: "M 400,200 L 480,200 L 480,280 L 400,280 Z" + }, + { + id: "kiraz", + name: "Kiraz", + path: "M 700,450 L 780,450 L 780,530 L 700,530 Z" + }, + { + id: "konak", + name: "Konak", + path: "M 410,450 L 480,450 L 480,520 L 410,520 Z" + }, + { + id: "menderes", + name: "Menderes", + path: "M 580,550 L 660,550 L 660,630 L 580,630 Z" + }, + { + id: "menemen", + name: "Menemen", + path: "M 380,280 L 470,280 L 470,370 L 380,370 Z" + }, + { + id: "narlidere", + name: "Narlıdere", + path: "M 360,500 L 420,500 L 420,560 L 360,560 Z" + }, + { + id: "odemis", + name: "Ödemiş", + path: "M 650,480 L 750,480 L 750,580 L 650,580 Z" + }, + { + id: "seferihisar", + name: "Seferihisar", + path: "M 480,580 L 560,580 L 560,660 L 480,660 Z" + }, + { + id: "selcuk", + name: "Selçuk", + path: "M 550,550 L 620,550 L 620,620 L 550,620 Z" + }, + { + id: "tire", + name: "Tire", + path: "M 600,600 L 680,600 L 680,680 L 600,680 Z" + }, + { + id: "torbali", + name: "Torbalı", + path: "M 580,500 L 660,500 L 660,580 L 580,580 Z" + }, + { + id: "urla", + name: "Urla", + path: "M 280,480 L 360,480 L 360,560 L 280,560 Z" + } + ] +}; diff --git a/src/index.tsx b/src/index.tsx index d473796..8b5ea75 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,5 +1,6 @@ import React, { useState, useCallback, MouseEventHandler } from 'react'; import Tooltip from './Tooltip'; +import CountyMapPopup, { CountyData, CountyType } from './CountyMapPopup'; import { Property } from 'csstype'; import { cities } from './data'; @@ -17,7 +18,11 @@ interface IProps { data?: Data, cityWrapper?: (cityComponent: React.ReactElement, city: CityType) => React.ReactElement, onHover?: (city: CityType) => void, - onClick?: (city: CityType) => void + onClick?: (city: CityType) => void, + showCountyMapOnClick?: boolean, + countyData?: Record, + countyMapWrapper?: (countyMapPopup: React.ReactElement, city: CityType, countyData: CountyData) => React.ReactElement, + onCountyClick?: (county: CountyType, city: CityType) => void } export type CityType = { id: string; plateNumber: number; name: string; path: string }; @@ -25,6 +30,9 @@ export type CustomStyleType = { idleColor: string, hoverColor: string }; export type ViewBoxType = { top: number; left: number; width: number; height: number }; type GetCitiesReturn = { element: React.ReactElement, cityType: CityType }; +// Re-export CountyData and CountyType for convenience +export type { CountyData, CountyType } from './CountyMapPopup'; + const TurkeyMap: React.FC = ({ viewBox = { top: 0, left: 80, width: 1050, height: 585 }, visible = true, @@ -35,9 +43,14 @@ const TurkeyMap: React.FC = ({ tooltipText, cityWrapper, onHover, - onClick + onClick, + showCountyMapOnClick = false, + countyData, + countyMapWrapper, + onCountyClick }) => { const [hoveredCityName, setHoveredCityName] = useState(undefined); + const [selectedCountyData, setSelectedCountyData] = useState<{ city: CityType, countyData: CountyData } | null>(null); const [tooltipStyle, setTooltipStyle] = useState<{ left: number, top: number, @@ -79,10 +92,20 @@ const TurkeyMap: React.FC = ({ }, [handleMouseEvent, handleHover]); const handleOnClick = useCallback((event: React.MouseEvent): void => { - if (onClick) { - handleMouseEvent(event, onClick); - } - }, [onClick, handleMouseEvent]); + const handleCityClick = (city: CityType) => { + // Call the original onClick handler if provided + if (onClick) { + onClick(city); + } + + // Show county map if enabled and county data is available + if (showCountyMapOnClick && countyData && countyData[city.id]) { + setSelectedCountyData({ city, countyData: countyData[city.id] }); + } + }; + + handleMouseEvent(event, handleCityClick); + }, [onClick, handleMouseEvent, showCountyMapOnClick, countyData]); const handleOnMouseMove: MouseEventHandler = useCallback((event) => { setTooltipStyle(prevState => ({ @@ -150,6 +173,16 @@ const TurkeyMap: React.FC = ({ }); }, [getCities, cityWrapper]); + const handleCloseCountyMap = useCallback(() => { + setSelectedCountyData(null); + }, []); + + const handleCountyClickInternal = useCallback((county: CountyType) => { + if (onCountyClick && selectedCountyData) { + onCountyClick(county, selectedCountyData.city); + } + }, [onCountyClick, selectedCountyData]); + const { top, left, width, height } = viewBox; return ( @@ -179,6 +212,24 @@ const TurkeyMap: React.FC = ({ } }`} } + + {/* County Map Popup */} + {selectedCountyData && (() => { + const countyMapPopup = ( + + ); + + return countyMapWrapper + ? countyMapWrapper(countyMapPopup, selectedCountyData.city, selectedCountyData.countyData) + : countyMapPopup; + })()} ); };