133 lines
3.8 KiB
TypeScript
133 lines
3.8 KiB
TypeScript
import { IconCircleFilled } from '@tabler/icons-react'
|
|
import * as TablerIcons from '@tabler/icons-react'
|
|
import type { IconProps } from '@tabler/icons-react'
|
|
import type { IconType } from 'react-icons'
|
|
import * as FontAwesomeIcons from 'react-icons/fa'
|
|
import type { ComponentType } from 'react'
|
|
|
|
import { PIN_COLORS } from '~/hooks/useMapMarkers'
|
|
import type { PinColorId } from '~/hooks/useMapMarkers'
|
|
|
|
interface MarkerPinProps {
|
|
color?: PinColorId | string | null
|
|
customColor?: string | null
|
|
icon?: string | null
|
|
iconColor?: string | null
|
|
visible?: boolean
|
|
active?: boolean
|
|
}
|
|
|
|
const resolvePinColor = (color?: PinColorId | string | null, customColor?: string | null) => {
|
|
if (customColor) return customColor
|
|
if (!color) return '#a84a12'
|
|
|
|
const preset = PIN_COLORS.find((pinColor) => pinColor.id === color)
|
|
return preset?.hex ?? color
|
|
}
|
|
|
|
const getContrastingIconColor = (backgroundColor: string) => {
|
|
const hex = backgroundColor.replace('#', '')
|
|
|
|
if (!/^[0-9a-fA-F]{6}$/.test(hex)) {
|
|
return '#ffffff'
|
|
}
|
|
|
|
const r = parseInt(hex.slice(0, 2), 16) / 255
|
|
const g = parseInt(hex.slice(2, 4), 16) / 255
|
|
const b = parseInt(hex.slice(4, 6), 16) / 255
|
|
|
|
const luminance =
|
|
0.2126 * r +
|
|
0.7152 * g +
|
|
0.0722 * b
|
|
|
|
return luminance > 0.55 ? '#111827' : '#ffffff'
|
|
}
|
|
|
|
type MarkerIconComponent =
|
|
| ComponentType<IconProps>
|
|
| IconType
|
|
|
|
const resolveIcon = (icon?: string | null): MarkerIconComponent => {
|
|
if (!icon) return IconCircleFilled
|
|
|
|
if (icon.startsWith('fa:')) {
|
|
const iconName = icon.replace('fa:', '')
|
|
const Icon = (FontAwesomeIcons as Record<string, unknown>)[iconName]
|
|
return Icon ? (Icon as IconType) : IconCircleFilled
|
|
}
|
|
|
|
if (icon.startsWith('tabler:')) {
|
|
const iconName = icon.replace('tabler:', '')
|
|
const Icon = (TablerIcons as Record<string, unknown>)[iconName]
|
|
return Icon ? (Icon as ComponentType<IconProps>) : IconCircleFilled
|
|
}
|
|
|
|
const Icon =
|
|
(TablerIcons as Record<string, unknown>)[icon] ??
|
|
(FontAwesomeIcons as Record<string, unknown>)[icon]
|
|
|
|
return Icon ? (Icon as MarkerIconComponent) : IconCircleFilled
|
|
}
|
|
|
|
export default function MarkerPin({
|
|
color = 'orange',
|
|
customColor,
|
|
icon,
|
|
iconColor,
|
|
visible = true,
|
|
active = false,
|
|
}: MarkerPinProps) {
|
|
if (!visible) return null
|
|
|
|
const resolvedColor = resolvePinColor(color, customColor)
|
|
const resolvedIconColor = iconColor ?? getContrastingIconColor(resolvedColor)
|
|
const Icon = resolveIcon(icon)
|
|
|
|
const width = active ? 42 : 36
|
|
const height = active ? 52 : 46
|
|
const iconSize = active ? 18 : 16
|
|
|
|
return (
|
|
<div
|
|
className="relative cursor-pointer"
|
|
style={{
|
|
width,
|
|
height,
|
|
filter: 'drop-shadow(0 1px 2px rgba(0,0,0,0.4))',
|
|
}}
|
|
>
|
|
<svg
|
|
width={width}
|
|
height={height}
|
|
viewBox="0 0 36 46"
|
|
fill="none"
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
aria-hidden="true"
|
|
>
|
|
<path
|
|
d="M18 45 C18 45 4 27.5 4 16.5 C4 7.4 10.3 1 18 1 C25.7 1 32 7.4 32 16.5 C32 27.5 18 45 18 45 Z"
|
|
fill={resolvedColor}
|
|
stroke="rgba(0,0,0,0.25)"
|
|
strokeWidth="1.5"
|
|
/>
|
|
|
|
<circle cx="18" cy="16.5" r="10.5" fill="rgba(255,255,255,0.18)" />
|
|
</svg>
|
|
|
|
<div
|
|
className="pointer-events-none absolute flex items-center justify-center"
|
|
style={{
|
|
left: '50%',
|
|
top: active ? 17 : 15,
|
|
transform: 'translate(-50%, -50%)',
|
|
width: iconSize,
|
|
height: iconSize,
|
|
}}
|
|
>
|
|
<Icon size={iconSize} color={resolvedIconColor} />
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|