diff --git a/.eslintrc.json b/.eslintrc.json index b67d5e8e..bd36b394 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -45,6 +45,7 @@ "version": "detect" }, "import/resolver": { + "typescript": {}, "alias": { "map": [["@", "./src/"]], "extensions": [".ts", ".tsx", ""] diff --git a/.vscode/settings.json b/.vscode/settings.json index 4d5f5344..e17edea7 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -5,7 +5,5 @@ "editor.tabSize": 2, "eslint.validate": ["javascript", "javascriptreact", "typescript", "typescriptreact"], "files.eol": "\n", - "cSpell.words": [ - "nextui" - ] + "cSpell.words": ["nextui"] } diff --git a/package.json b/package.json index 17b12216..886dbd79 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "@tanstack/react-query": "^5.59.17", "clsx": "2.1.1", "framer-motion": "~11.1.1", + "geojson": "^0.5.0", "highcharts": "^11.4.8", "highcharts-react-official": "^3.2.1", "iconsax-react": "^0.0.8", @@ -58,6 +59,7 @@ "devDependencies": { "@commitlint/cli": "^19.5.0", "@commitlint/config-conventional": "^19.5.0", + "@types/geojson": "^7946.0.14", "@types/leaflet": "^1.9.14", "@types/node": "20.5.7", "@types/react": "^18.3.12", diff --git a/src/components/Map/CountryPolygon.tsx b/src/components/Map/CountryPolygon.tsx deleted file mode 100644 index be2ca163..00000000 --- a/src/components/Map/CountryPolygon.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { useState } from 'react'; -import { Polygon } from 'react-leaflet'; - -import { CountryMapData } from '@/domain/entities/country/CountryMapData'; - -import { CountryPopup } from './CountryPopup'; - -export function CountryPolygon({ country }: { country: CountryMapData }) { - const [active, setActive] = useState(false); - - return ( - <> - setActive(true) }} - /> - {active && setActive(false)} />} - - ); -} diff --git a/src/components/Map/CountryPopup.tsx b/src/components/Map/CountryPopup.tsx deleted file mode 100644 index 20f4cb17..00000000 --- a/src/components/Map/CountryPopup.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { Popup } from 'react-leaflet'; - -import { CountryMapData } from '@/domain/entities/country/CountryMapData'; -import { useCountryDataQuery } from '@/domain/hooks/countryHooks'; - -/** - * This is just an example of how the sidebars could fetch the data when a country is clicked - */ -export function CountryPopup({ country, onClose }: { country: CountryMapData; onClose: () => void }) { - const { data, isPending } = useCountryDataQuery(country.properties.adm0_id); - - return ( - - FCS: {isPending || !data ? 'Loading...' : data.fcs} - - ); -} diff --git a/src/components/Map/Map.tsx b/src/components/Map/Map.tsx index a34c9460..769f7461 100644 --- a/src/components/Map/Map.tsx +++ b/src/components/Map/Map.tsx @@ -1,14 +1,71 @@ -'use client'; - import 'leaflet/dist/leaflet.css'; -import { MapContainer, TileLayer, ZoomControl } from 'react-leaflet'; +import { Feature, FeatureCollection } from 'geojson'; +import { LeafletMouseEvent } from 'leaflet'; +import { GeoJSON, MapContainer, TileLayer, ZoomControl } from 'react-leaflet'; +import { CountryMapData } from '@/domain/entities/country/CountryMapData.ts'; import { MapProps } from '@/domain/props/MapProps'; -import { CountryPolygon } from './CountryPolygon'; - export default function Map({ countries }: MapProps) { + const countryStyle: L.PathOptions = { + fillColor: 'var(--color-active-countries)', + weight: 0.5, + color: 'var(--color-background)', + fillOpacity: 0.4, + }; + + const highlightCountry = (event: LeafletMouseEvent) => { + const layer = event.target; + const countryData: CountryMapData = layer.feature as CountryMapData; + if (countryData.properties.interactive) { + layer.setStyle({ + fillColor: 'var(--color-hover)', + fillOpacity: 0.8, + }); + } else { + layer.getElement().style.cursor = 'grab'; + } + }; + + const resetHighlight = (event: LeafletMouseEvent) => { + const layer = event.target; + const countryData: CountryMapData = layer.feature as CountryMapData; + if (countryData.properties.interactive) { + layer.setStyle(countryStyle); + } + }; + + const onCountryClick = (event: LeafletMouseEvent) => { + const countryData: CountryMapData = event.target.feature as CountryMapData; + if (countryData.properties.interactive) { + alert(`You clicked on ${countryData.properties.adm0_name}`); + } + }; + + const onEachCountry = (country: Feature, layer: L.Layer) => { + if ((layer as L.GeoJSON).feature) { + const leafletLayer = layer as L.Path; + leafletLayer.setStyle(countryStyle); + if (!(country as CountryMapData).properties.interactive) { + leafletLayer.setStyle({ fillColor: 'var(--color-inactive-countries)', fillOpacity: 0.85 }); + } + leafletLayer.on({ + mouseover: highlightCountry, + mouseout: resetHighlight, + click: onCountryClick, + mousedown: () => { + const element = leafletLayer.getElement() as HTMLElement | null; + if (element) element.style.cursor = 'grabbing'; + }, + mouseup: () => { + const element = leafletLayer.getElement() as HTMLElement | null; + if (element) element.style.cursor = 'grab'; + }, + }); + } + }; + return ( + {countries && } - {countries.features - .filter((c) => c.properties.interactive) - .map((c) => ( - // TODO fix the layout, this is just an example - - ))} ); } diff --git a/src/styles/globals.css b/src/styles/globals.css index d171f385..8430d90f 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -18,4 +18,4 @@ canvas.react-pdf__Page__canvas { .dot-delay-3 { animation-delay: 0.6s; -} \ No newline at end of file +} diff --git a/tailwind.config.js b/tailwind.config.js index cb1bbfe6..5bb214e4 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -3,9 +3,19 @@ import { nextui } from '@nextui-org/theme'; /** @type {import('tailwindcss').Config} */ module.exports = { safelist: [ - 'w-[215px]', 'w-[179px]', 'w-[636px]', 'w-[500px]', - 'h-[657px]', 'h-[600px]', 'rounded-[12px_0_0_12px]', - 'max-w-[80%]', 'max-w-[250px]', 'max-w-[400px]', 'pl-[215px]', 'pl-[179px]', 'z-[9999]' + 'w-[215px]', + 'w-[179px]', + 'w-[636px]', + 'w-[500px]', + 'h-[657px]', + 'h-[600px]', + 'rounded-[12px_0_0_12px]', + 'max-w-[80%]', + 'max-w-[250px]', + 'max-w-[400px]', + 'pl-[215px]', + 'pl-[179px]', + 'z-[9999]', ], content: [ './src/components/**/*.{js,ts,jsx,tsx,mdx}', @@ -43,7 +53,7 @@ module.exports = { '12px_0_0_12px': '12px 0 0 12px', }, zIndex: { - '9999': '9999', + 9999: '9999', }, animation: { blink: 'blink 1s step-end infinite', @@ -56,12 +66,22 @@ module.exports = { pulse: { '0%, 100%': { transform: 'scale(1)', opacity: '0.6' }, '50%': { transform: 'scale(1.3)', opacity: '1' }, - } + }, }, }, }, darkMode: 'class', plugins: [ + function ({ addBase, theme }) { + addBase({ + ':root': { + '--color-hover': theme('colors.hover'), + '--color-background': theme('colors.background'), + '--color-active-countries': theme('colors.activeCountries'), + '--color-inactive-countries': theme('colors.inactiveCountries'), + }, + }); + }, nextui({ themes: { light: { @@ -89,6 +109,8 @@ module.exports = { chatbotUserMsg: '#E6F1FE', chatbotDivider: '#292d32', surfaceGrey: '#B0B0B0', + activeCountries: '#82bce0', + inactiveCountries: '#a7b3ba', }, }, dark: { @@ -116,6 +138,8 @@ module.exports = { chatbotUserMsg: '#26262A', chatbotDivider: '#556372', surfaceGrey: '#444444', + activeCountries: '#115884', + inactiveCountries: '#85929b', }, }, }, diff --git a/yarn.lock b/yarn.lock index 0691546e..0e34c1e0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2774,7 +2774,7 @@ dependencies: "@types/node" "*" -"@types/geojson@*": +"@types/geojson@*", "@types/geojson@^7946.0.14": version "7946.0.14" resolved "https://registry.yarnpkg.com/@types/geojson/-/geojson-7946.0.14.tgz#319b63ad6df705ee2a65a73ef042c8271e696613" integrity sha512-WCfD5Ht3ZesJUsONdhvm84dmzWOiOzOAqOncN0++w0lBw1o8OuDNJF2McvvCef/yBqb/HYRahp1BYtODFQ8bRg== @@ -4216,6 +4216,11 @@ gauge@^3.0.0: strip-ansi "^6.0.1" wide-align "^1.1.2" +geojson@^0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/geojson/-/geojson-0.5.0.tgz#3cd6c96399be65b56ee55596116fe9191ce701c0" + integrity sha512-/Bx5lEn+qRF4TfQ5aLu6NH+UKtvIv7Lhc487y/c8BdludrCTpiWf9wyI0RTyqg49MFefIAvFDuEi5Dfd/zgNxQ== + get-caller-file@^2.0.5: version "2.0.5" resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e"