Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 85 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,13 +87,94 @@ 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
};

<TurkeyMap
showCountyMapOnClick={true}
countyData={countyData}
/>
```

#### Handling County Events

```javascript
<TurkeyMap
showCountyMapOnClick={true}
countyData={countyData}
onCountyClick={(county, city) => {
console.log(`${county.name} in ${city.name} was clicked!`);
}}
/>
```

#### Custom County Map Wrapper

```javascript
const customCountyMapWrapper = (countyMapPopup, city, countyData) => (
<div className="my-custom-wrapper">
<h3>Custom Header for {city.name}</h3>
{countyMapPopup}
</div>
);

<TurkeyMap
showCountyMapOnClick={true}
countyData={countyData}
countyMapWrapper={customCountyMapWrapper}
/>
```

#### 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..." }
]
};

<TurkeyMap
showCountyMapOnClick={true}
countyData={{ mycity: myCustomCountyData }}
/>
```

**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

| 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* } |

Expand All @@ -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<string, CountyData>* | *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

Expand Down
255 changes: 255 additions & 0 deletions src/CountyMapPopup.tsx
Original file line number Diff line number Diff line change
@@ -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<ICountyMapPopupProps> = ({
countyData,
onClose,
customStyle = { idleColor: "#444", hoverColor: "#dc3522" },
hoverable = true,
showTooltip = false,
tooltipText,
onCountyHover,
onCountyClick
}) => {
const [hoveredCountyName, setHoveredCountyName] = useState<string | undefined>(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<SVGGElement, 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<SVGGElement, MouseEvent>): void => {
handleMouseEvent(event, handleHover);
}, [handleMouseEvent, handleHover]);

const handleOnClick = useCallback((event: React.MouseEvent<SVGGElement, 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<SVGGElement, 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<SVGGElement, 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<HTMLDivElement>) => {
if (event.target === event.currentTarget) {
onClose();
}
}, [onClose]);

return (
<div
role="dialog"
aria-modal="true"
aria-labelledby="county-map-title"
style={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
zIndex: 1000
}}
onClick={handleOverlayClick}
>
<div
style={{
backgroundColor: 'white',
borderRadius: '8px',
padding: '20px',
maxWidth: '90%',
maxHeight: '90%',
overflow: 'auto',
position: 'relative'
}}
>
<div style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: '10px'
}}>
<h2 id="county-map-title" style={{ margin: 0, fontSize: '1.5em' }}>{countyData.cityName} - İlçeler</h2>
<button
onClick={onClose}
style={{
background: 'none',
border: 'none',
fontSize: '1.5em',
cursor: 'pointer',
padding: '0 10px'
}}
aria-label="Close county map"
>
×
</button>
</div>

<div id="county-map-container" style={{ maxWidth: 800, margin: "0 auto", textAlign: 'center' }}>
{showTooltip && (
<div
id="county-map-tooltip"
style={{
...tooltipStyle,
position: 'absolute',
padding: '5px 10px',
backgroundColor: 'rgba(0, 0, 0, 0.75)',
color: 'white',
borderRadius: '4px',
fontSize: '14px',
pointerEvents: 'none',
zIndex: 1001
}}
>
{tooltipText || hoveredCountyName}
</div>
)}

<svg
version="1.1"
xmlns="http://www.w3.org/2000/svg"
xmlnsXlink="http://www.w3.org/1999/xlink"
viewBox="0 0 1000 1000"
xmlSpace="preserve"
style={{ width: "100%", height: "auto" }}
>
<g key={countyData.cityId} id={countyData.cityId}>
{countyData.counties.map((county) => (
<g
key={county.id}
id={county.id}
data-ilceadi={county.name}
onMouseEnter={handleOnMouseEnter}
onMouseLeave={handleOnMouseLeave}
onMouseOver={event => hoverable ? handleOnHover(event) : undefined}
onMouseMove={showTooltip ? handleOnMouseMove : undefined}
onClick={handleOnClick}
>
<path style={{ cursor: "pointer", fill: customStyle.idleColor }} d={county.path} />
</g>
))}
</g>
</svg>

{showTooltip && <style>
{`@keyframes county_react_map_tooltip_fade_out {
0% {
opacity: 1;
}
100% {
visibility: hidden;
opacity: 0;
}
}`}
</style>}
</div>
</div>
</div>
);
};

export default CountyMapPopup;
Loading