diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5e3650e..95a9e5d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -110,6 +110,9 @@ importers: filesize: specifier: ^10.0.12 version: 10.0.12 + leaflet: + specifier: ^1.9.4 + version: 1.9.4 lodash-es: specifier: ^4.17.21 version: 4.17.21 @@ -134,6 +137,9 @@ importers: react-icons: specifier: ^4.12.0 version: 4.12.0(react@18.2.0) + react-leaflet: + specifier: ^4.2.1 + version: 4.2.1(leaflet@1.9.4)(react-dom@18.2.0)(react@18.2.0) react-resizable: specifier: ^3.0.5 version: 3.0.5(react-dom@18.2.0)(react@18.2.0) @@ -159,6 +165,9 @@ importers: specifier: ^4.4.1 version: 4.4.1(@types/react@18.2.21)(react@18.2.0) devDependencies: + '@types/leaflet': + specifier: ^1.9.8 + version: 1.9.8 '@types/loadable__component': specifier: ^5.13.8 version: 5.13.8 @@ -6498,6 +6507,18 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /@react-leaflet/core@2.1.0(leaflet@1.9.4)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-Qk7Pfu8BSarKGqILj4x7bCSZ1pjuAPZ+qmRwH5S7mDS91VSbVVsJSrW4qA+GPrro8t69gFYVMWb1Zc4yFmPiVg==} + peerDependencies: + leaflet: ^1.9.0 + react: ^18.0.0 + react-dom: ^18.0.0 + dependencies: + leaflet: 1.9.4 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /@reduxjs/toolkit@1.9.7(react-redux@7.2.9)(react@17.0.2): resolution: {integrity: sha512-t7v8ZPxhhKgOKtU+uyJT13lu4vL7az5aFi4IdoDs/eS548edn2M8Ik9h8fxgvMjGoAUVFSt6ZC1P5cWmQ014QQ==} peerDependencies: @@ -8108,7 +8129,6 @@ packages: /@types/geojson@7946.0.13: resolution: {integrity: sha512-bmrNrgKMOhM3WsafmbGmC+6dsF2Z308vLFsQ3a/bT8X8Sv5clVYpPars/UPq+sAaJP+5OoLAYgwbkS5QEJdLUQ==} - dev: false /@types/geojson@7946.0.8: resolution: {integrity: sha512-1rkryxURpr6aWP7R786/UQOkJ3PcpQiWkAXBmdWc7ryFWqN6a4xfK7BtjXvFBKO9LjQ+MWQSWxYeZX1OApnArA==} @@ -8189,6 +8209,12 @@ packages: '@types/node': 18.17.12 dev: false + /@types/leaflet@1.9.8: + resolution: {integrity: sha512-EXdsL4EhoUtGm2GC2ZYtXn+Fzc6pluVgagvo2VC1RHWToLGlTRwVYoDpqS/7QXa01rmDyBjJk3Catpf60VMkwg==} + dependencies: + '@types/geojson': 7946.0.13 + dev: true + /@types/loadable__component@5.13.8: resolution: {integrity: sha512-0FF/WihuPkR5IFOHiBzC95bSACvgQNUQCuNy1WF8F/lCBBHgS2SxarIk4CTjWM10A72ovpmXZDRcuAXZNS+/kQ==} dependencies: @@ -14335,6 +14361,10 @@ packages: readable-stream: 2.3.8 dev: true + /leaflet@1.9.4: + resolution: {integrity: sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==} + dev: false + /less@4.2.0: resolution: {integrity: sha512-P3b3HJDBtSzsXUl0im2L7gTO5Ubg8mEN6G8qoTS77iXxXX4Hvu4Qj540PZDvQ8V6DmX6iXo98k7Md0Cm1PrLaA==} engines: {node: '>=6'} @@ -18895,6 +18925,19 @@ packages: - encoding dev: false + /react-leaflet@4.2.1(leaflet@1.9.4)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-p9chkvhcKrWn/H/1FFeVSqLdReGwn2qmiobOQGO3BifX+/vV/39qhY8dGqbdcPh1e6jxh/QHriLXr7a4eLFK4Q==} + peerDependencies: + leaflet: ^1.9.0 + react: ^18.0.0 + react-dom: ^18.0.0 + dependencies: + '@react-leaflet/core': 2.1.0(leaflet@1.9.4)(react-dom@18.2.0)(react@18.2.0) + leaflet: 1.9.4 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /react-lifecycles-compat@3.0.4: resolution: {integrity: sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==} dev: false diff --git a/src/client/components/website/WebsiteVisitorMap.tsx b/src/client/components/website/WebsiteVisitorMap.tsx deleted file mode 100644 index e5f899f..0000000 --- a/src/client/components/website/WebsiteVisitorMap.tsx +++ /dev/null @@ -1,95 +0,0 @@ -import type { LarkMapProps, PointLayerProps } from '@antv/larkmap'; -import { FullscreenControl, LarkMap, PointLayer } from '@antv/larkmap'; -import React from 'react'; -import { useSettingsStore } from '../../store/settings'; -import { useGlobalConfig } from '../../hooks/useConfig'; -import { trpc } from '../../api/trpc'; -import { useCurrentWorkspaceId } from '../../store/user'; -import { useGlobalRangeDate } from '../../hooks/useGlobalRangeDate'; - -const layerOptions: Omit = { - autoFit: true, - shape: 'circle', - size: 5, - blend: 'additive', - color: { - field: 'count', - value: [ - 'rgb(102,37,6)', - 'rgb(153,52,4)', - 'rgb(204,76,2)', - 'rgb(236,112,20)', - 'rgb(254,153,41)', - 'rgb(254,196,79)', - 'rgb(254,227,145)', - ], - }, -}; - -function useMapConfig(mapType: 'Mapbox' | 'Gaode' = 'Mapbox'): LarkMapProps { - const { amapToken, mapboxToken } = useGlobalConfig(); - const colorScheme = useSettingsStore((state) => state.colorScheme); - - const baseOption: LarkMapProps['mapOptions'] = { - center: [120.210792, 30.246026], - zoom: 0, - }; - - if (mapType === 'Gaode') { - return { - mapType: 'Gaode', - mapOptions: { - ...baseOption, - style: - colorScheme === 'light' - ? 'amap://styles/light' - : 'amap://styles/dark', - token: amapToken, - }, - logoVisible: false, - }; - } else { - return { - mapType: 'Mapbox', - mapOptions: { - ...baseOption, - style: colorScheme === 'light' ? 'light' : 'dark', - token: mapboxToken, - }, - logoVisible: false, - }; - } -} - -interface WebsiteVisitorMapProps { - websiteId: string; -} -export const WebsiteVisitorMap: React.FC = React.memo( - (props) => { - const config = useMapConfig(); - const workspaceId = useCurrentWorkspaceId(); - const { startDate, endDate } = useGlobalRangeDate(); - const startAt = startDate.valueOf(); - const endAt = endDate.valueOf(); - - const { data } = trpc.website.geoStats.useQuery({ - workspaceId, - websiteId: props.websiteId, - startAt, - endAt, - }); - - const source: PointLayerProps['source'] = { - data: data ?? [], - parser: { type: 'json', x: 'longitude', y: 'latitude' }, - }; - - return ( - - - - - ); - } -); -WebsiteVisitorMap.displayName = 'WebsiteVisitorMap'; diff --git a/src/client/components/website/WebsiteVisitorMap/VisitorLarkMap.tsx b/src/client/components/website/WebsiteVisitorMap/VisitorLarkMap.tsx new file mode 100644 index 0000000..aa035d9 --- /dev/null +++ b/src/client/components/website/WebsiteVisitorMap/VisitorLarkMap.tsx @@ -0,0 +1,86 @@ +import { + FullscreenControl, + LarkMap, + LarkMapProps, + PointLayer, + PointLayerProps, +} from '@antv/larkmap'; +import React from 'react'; +import { AppRouterOutput } from '../../../api/trpc'; +import { useGlobalConfig } from '../../../hooks/useConfig'; +import { useSettingsStore } from '../../../store/settings'; +import { mapCenter } from './utils'; + +const layerOptions: Omit = { + autoFit: true, + shape: 'circle', + size: 5, + blend: 'additive', + color: { + field: 'count', + value: [ + 'rgb(102,37,6)', + 'rgb(153,52,4)', + 'rgb(204,76,2)', + 'rgb(236,112,20)', + 'rgb(254,153,41)', + 'rgb(254,196,79)', + 'rgb(254,227,145)', + ], + }, +}; + +function useMapConfig(mapType: 'Mapbox' | 'Gaode' = 'Mapbox'): LarkMapProps { + const { amapToken, mapboxToken } = useGlobalConfig(); + const colorScheme = useSettingsStore((state) => state.colorScheme); + + const baseOption: LarkMapProps['mapOptions'] = { + center: [mapCenter.lng, mapCenter.lat], + zoom: 0, + }; + + if (mapType === 'Gaode') { + return { + mapType: 'Gaode', + mapOptions: { + ...baseOption, + style: + colorScheme === 'light' + ? 'amap://styles/light' + : 'amap://styles/dark', + token: amapToken, + }, + logoVisible: false, + }; + } else { + return { + mapType: 'Mapbox', + mapOptions: { + ...baseOption, + style: colorScheme === 'light' ? 'light' : 'dark', + token: mapboxToken, + }, + logoVisible: false, + }; + } +} + +export const VisitorLarkMap: React.FC<{ + data: AppRouterOutput['website']['geoStats']; + mapType: 'Mapbox' | 'Gaode'; +}> = React.memo((props) => { + const config = useMapConfig(props.mapType); + + const source: PointLayerProps['source'] = { + data: props.data ?? [], + parser: { type: 'json', x: 'longitude', y: 'latitude' }, + }; + + return ( + + + + + ); +}); +VisitorLarkMap.displayName = 'VisitorLarkMap'; diff --git a/src/client/components/website/WebsiteVisitorMap/VisitorLeafletMap.css b/src/client/components/website/WebsiteVisitorMap/VisitorLeafletMap.css new file mode 100644 index 0000000..bd8f2cb --- /dev/null +++ b/src/client/components/website/WebsiteVisitorMap/VisitorLeafletMap.css @@ -0,0 +1,3 @@ +.leaflet-control-attribution.leaflet-control { + display: none; +} diff --git a/src/client/components/website/WebsiteVisitorMap/VisitorLeafletMap.tsx b/src/client/components/website/WebsiteVisitorMap/VisitorLeafletMap.tsx new file mode 100644 index 0000000..a427f26 --- /dev/null +++ b/src/client/components/website/WebsiteVisitorMap/VisitorLeafletMap.tsx @@ -0,0 +1,59 @@ +import React from 'react'; +import { AppRouterOutput } from '../../../api/trpc'; +import { + MapContainer, + CircleMarker, + Popup, + TileLayer, + useMap, +} from 'react-leaflet'; +import { mapCenter } from './utils'; +import 'leaflet/dist/leaflet.css'; +import './VisitorLeafletMap.css'; + +export const UserDataPoint: React.FC<{ + longitude: number; + latitude: number; + count: number; +}> = React.memo((props) => { + const map = useMap(); + + return ( + + {props.count} users + + ); +}); +UserDataPoint.displayName = 'UserDataPoint'; + +export const VisitorLeafletMap: React.FC<{ + data: AppRouterOutput['website']['geoStats']; +}> = React.memo((props) => { + return ( + + + + {props.data.map((item) => ( + + ))} + + ); +}); +VisitorLeafletMap.displayName = 'VisitorLeafletMap'; diff --git a/src/client/components/website/WebsiteVisitorMap/index.tsx b/src/client/components/website/WebsiteVisitorMap/index.tsx new file mode 100644 index 0000000..2332991 --- /dev/null +++ b/src/client/components/website/WebsiteVisitorMap/index.tsx @@ -0,0 +1,58 @@ +import React from 'react'; +import { trpc } from '../../../api/trpc'; +import { useCurrentWorkspaceId } from '../../../store/user'; +import { useGlobalRangeDate } from '../../../hooks/useGlobalRangeDate'; +import loadable from '@loadable/component'; +import { Loading } from '../../Loading'; +import { useGlobalConfig } from '../../../hooks/useConfig'; + +const VisitorLeafletMap = loadable(() => + import('./VisitorLeafletMap').then((m) => m.VisitorLeafletMap) +); + +const VisitorLarkMap = loadable(() => + import('./VisitorLarkMap').then((m) => m.VisitorLarkMap) +); + +function useMapType() { + const { amapToken, mapboxToken } = useGlobalConfig(); + + if (mapboxToken) { + return 'Mapbox'; + } else if (amapToken) { + return 'Gaode'; + } else { + return 'Leaflet'; + } +} + +interface WebsiteVisitorMapProps { + websiteId: string; +} +export const WebsiteVisitorMap: React.FC = React.memo( + (props) => { + const workspaceId = useCurrentWorkspaceId(); + const { startDate, endDate } = useGlobalRangeDate(); + const startAt = startDate.valueOf(); + const endAt = endDate.valueOf(); + const { data } = trpc.website.geoStats.useQuery({ + workspaceId, + websiteId: props.websiteId, + startAt, + endAt, + }); + + const mapType = useMapType(); + + if (!data) { + return ; + } + + if (mapType === 'Leaflet') { + return ; + } + + return ; + } +); +WebsiteVisitorMap.displayName = 'WebsiteVisitorMap'; diff --git a/src/client/components/website/WebsiteVisitorMap/utils.ts b/src/client/components/website/WebsiteVisitorMap/utils.ts new file mode 100644 index 0000000..8acf828 --- /dev/null +++ b/src/client/components/website/WebsiteVisitorMap/utils.ts @@ -0,0 +1,4 @@ +export const mapCenter = { + lat: 30.246026, + lng: 120.210792, +}; diff --git a/src/client/package.json b/src/client/package.json index 5b18f3d..fdac1a5 100644 --- a/src/client/package.json +++ b/src/client/package.json @@ -32,6 +32,7 @@ "dayjs": "^1.11.9", "eventemitter-strict": "^1.0.1", "filesize": "^10.0.12", + "leaflet": "^1.9.4", "lodash-es": "^4.17.21", "millify": "^6.1.0", "pretty-ms": "^9.0.0", @@ -40,6 +41,7 @@ "react-easy-sort": "^1.5.3", "react-grid-layout": "1.4.2", "react-icons": "^4.12.0", + "react-leaflet": "^4.2.1", "react-resizable": "^3.0.5", "react-router": "^6.15.0", "react-router-dom": "^6.15.0", @@ -50,6 +52,7 @@ "zustand": "^4.4.1" }, "devDependencies": { + "@types/leaflet": "^1.9.8", "@types/loadable__component": "^5.13.8", "@types/lodash-es": "^4.17.12", "@types/react": "^18.2.21",