diff --git a/.github/workflows/deploy-preview.yml b/.github/workflows/deploy-preview.yml index cd767d58..b82b70a4 100644 --- a/.github/workflows/deploy-preview.yml +++ b/.github/workflows/deploy-preview.yml @@ -38,6 +38,9 @@ jobs: - name: Install ๐ŸŽฏ run: cd site; npm install + - name: Lint ๐Ÿงน + run: cd site; npm run lint + - name: Build ๐Ÿƒ run: cd site; npm run build env: diff --git a/site/package.json b/site/package.json index f20c3226..bd385357 100644 --- a/site/package.json +++ b/site/package.json @@ -7,7 +7,6 @@ "dev": "vite", "build": "vite build", "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0", - "aws_deploy": "python3 scripts/deploy.py", "preview": "vite preview" }, "dependencies": { diff --git a/site/scripts/deploy.py b/site/scripts/deploy.py deleted file mode 100644 index b441bfdf..00000000 --- a/site/scripts/deploy.py +++ /dev/null @@ -1,61 +0,0 @@ -# Copyright(c) Meta Platforms, Inc. and affiliates. - -import errno -import os -import shutil -import subprocess -import sys - -"""Script to upload Aria app to S3 bucket -Usage: python3 aws_deploy.py - -Assumptions: -1) You have the aws cli installed, and it can get access to credentials -1a) This can be anything specified here: https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-envvars.html -1b) For github actions, you should have AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY set as secrets, with AWS_DEFAULT_REGION in the environment -2) You have installed python 3. -3) You are running from within the root directory of the repo (and therefore need to look inside the 'site' folder to start work) -4) Environment variables NODE_VERSION, DISTDIR, and IDENTIFIER are defined -See .github/workflows/deploy.yml 'Setup Vars' step for how those env vars are created. -""" - -def deploy(): - print("Calculating build hash and distdir...") - output = subprocess.run( - ["git", "rev-parse", "--short", "HEAD"], capture_output=True - ) - hash = output.stdout.decode("utf-8").strip() - print("\ngithash: " + hash) - identifier = os.environ["IDENTIFIER"] - distdir = os.environ["DISTDIR"] - print("\ndistdir: " + distdir) - # Blow away the previous dir, if any. - if os.path.exists(distdir): - print(f"Previous distribution dir {distdir} found, removing.") - shutil.rmtree(distdir) - print(f"\nCreating dist folder {distdir}") - try: - os.mkdir(distdir) - except OSError as err: - if err.errno == errno.EEXIST: - print(f"{distdir} already exists") - elif err.errno == errno.EACCES: - print(f"{distdir} permission denied") - raise - print("\nCopying dist folder") - subprocess.check_call(args=f"cp -a ./dist/* {distdir}", shell=True) - # print("\nPrepping index.html with correct asset, css, and javascript paths") - # index = "dist/index.html" - # newindex = os.path.join(distdir, "index.html") - # with open(index, "r") as input: - # with open(newindex, "w+") as output: - # # Massage the paths appropriately - # for s in input: - # s = ( - # s.replace("/assets/", f"/aria/{distdir}/assets/") - # .replace("/favicon.png", f"/aria/{distdir}/favicon.png") - # ) - # output.write(s) - -if __name__ == "__main__": - deploy() diff --git a/site/src/App.jsx b/site/src/App.jsx index 98739232..86055963 100644 --- a/site/src/App.jsx +++ b/site/src/App.jsx @@ -7,7 +7,7 @@ import { useState, useEffect, useRef } from "react"; import Tour from "./Tour"; import StartupBox from "./StartupBox"; import { ThemeProvider } from "@mui/material"; -import { useNavigatorState } from "./navigator/Navigator"; +import { useNavigatorState } from "./navigator/useNavigatorState"; function App() { const [modeName, setModeName] = useState(getTheme()); @@ -30,7 +30,7 @@ function App() { localStorage.setItem("tour", event.target.checked); setTour(!tour); }; - + const [visibleTypes, setVisibleTypes] = useState([]); diff --git a/site/src/Map.jsx b/site/src/Map.jsx index b86073e8..1a2dda5a 100644 --- a/site/src/Map.jsx +++ b/site/src/Map.jsx @@ -3,6 +3,7 @@ import { NavigationControl, Source, AttributionControl, + ScaleControl, } from "react-map-gl/maplibre"; import "maplibre-gl/dist/maplibre-gl.css"; import * as pmtiles from "pmtiles"; @@ -176,7 +177,7 @@ export default function Map({ setActiveFeature(null); } }, - [visibleTypes] + [visibleTypes, setFeatures, setActiveFeature] ); const handleZoom = (event) => { @@ -222,32 +223,43 @@ export default function Map({ /> {[false, true].map((label) => { - return layers.map((props, i) => ( - - )); + return layers.map((layerProps, i) => { + const { + theme, + type, + color, + activeColor, + activeOnly, + ...otherProps + } = layerProps; + + return ( + + ); + }); })} + @@ -364,7 +377,6 @@ export default function Map({ /> )} { const [anchorEl, setAnchorEl] = useState(null); @@ -102,8 +102,13 @@ const ThemeSelector = ({ setSelectedThemes(newSelectedThemes); setSelectedTypesState(newSelectedTypes); - updateVisibleTypes(newSelectedTypes); - }, []); + + // Update visible types inline to avoid dependency issues + const visible = Object.keys(newSelectedTypes).filter( + (type) => newSelectedTypes[type] + ); + setVisibleTypes(visible); + }, [setVisibleTypes]); useEffect(() => { const newSelectedTypes = {}; @@ -360,4 +365,13 @@ const ThemeSelector = ({ ); }; +ThemeSelector.propTypes = { + mode: PropTypes.string.isRequired, + visibleTypes: PropTypes.array.isRequired, + setVisibleTypes: PropTypes.func.isRequired, + activeThemes: PropTypes.array.isRequired, + setActiveThemes: PropTypes.func.isRequired, + themeRef: PropTypes.object.isRequired, +}; + export default ThemeSelector; diff --git a/site/src/ThemeTypeLayer.jsx b/site/src/ThemeTypeLayer.jsx index ad6cafd6..dffa1082 100644 --- a/site/src/ThemeTypeLayer.jsx +++ b/site/src/ThemeTypeLayer.jsx @@ -257,6 +257,7 @@ ThemeTypeLayer.propTypes = { line: PropTypes.bool, polygon: PropTypes.bool, extrusion: PropTypes.bool, + visible: PropTypes.bool, outline: PropTypes.bool, active: PropTypes.bool, label: PropTypes.bool, diff --git a/site/src/Tour.jsx b/site/src/Tour.jsx index e469fe16..c2d3e94e 100644 --- a/site/src/Tour.jsx +++ b/site/src/Tour.jsx @@ -1,6 +1,6 @@ +import PropTypes from "prop-types"; import Joyride, { ACTIONS, EVENTS, LIFECYCLE } from "react-joyride"; import { useState } from "react"; -import LayerIcon from "./icons/icon-layers.svg?react"; import "./Tour.css"; const Steps = [ @@ -93,62 +93,6 @@ const Steps = [ }, ]; -const sampleFeature = { - geometry: { - type: "Point", - coordinates: [3.7302714586257935, 51.05027395815554], - }, - type: "Feature", - properties: { - theme: "places", - type: "place", - id: "08f194db132d2b6d0388899915aac1fc", - "@name": "Grill Mix Centrum", - "@category": "bar_and_grill_restaurant", - names: '{"primary":"Grill Mix Centrum","common":null,"rules":null}', - confidence: 0.9584614231086451, - categories: - '{"main":"bar_and_grill_restaurant","alternate":["pizza_restaurant","doner_kebab"]}', - websites: '["http://www.pizzacity.be"]', - socials: '["https://www.facebook.com/612060042296776"]', - phones: '["+3292257440"]', - addresses: - '[{"freeform":"Vlaanderenstraat 85","locality":"Gent","postcode":"9000","region":null,"country":"BE"}]', - version: 0, - update_time: "2024-04-11T00:00:00.000Z", - sources: - '[{"property":"","dataset":"meta","record_id":"612060042296776","confidence":null}]', - }, - id: 38848842, - layer: { - id: "places", - type: "circle", - source: "overture-places", - "source-layer": "places", - filter: [">=", ["get", "confidence"], 0], - layout: {}, - paint: { - "circle-color": { - r: 0.792156862745098, - g: 0.6980392156862745, - b: 0.8392156862745098, - a: 1, - }, - "circle-radius": 1.8749883174673414, - "circle-stroke-width": 2, - "circle-stroke-color": { - r: 0, - g: 0, - b: 0, - a: 1, - }, - }, - }, - source: "overture-places", - sourceLayer: "places", - state: {}, -}; - function Tour({ run, modeName, setFeatures, setNavigatorOpen, themeRef }) { const [stepIndex, setStepIndex] = useState(0); @@ -164,8 +108,8 @@ function Tour({ run, modeName, setFeatures, setNavigatorOpen, themeRef }) { Each "step" has typically 3 events associated with it. Therefore, we must check that events only take place once, which is why we check the event lifecycle. In addition, we must check the action of the event (next, prev, skip, etc). This is a tedious and granular process, but it allows the tour - to be very controlled and open/close different parts and pieces of the explorer site to show them - all off. + to be very controlled and open/close different parts and pieces of the explorer site to show them + all off. */ const handleJoyrideCallback = (event) => { if ([EVENTS.STEP_AFTER, EVENTS.TARGET_NOT_FOUND].includes(event.type)) { @@ -291,4 +235,12 @@ function Tour({ run, modeName, setFeatures, setNavigatorOpen, themeRef }) { ); } +Tour.propTypes = { + run: PropTypes.bool.isRequired, + modeName: PropTypes.string.isRequired, + setFeatures: PropTypes.func.isRequired, + setNavigatorOpen: PropTypes.func.isRequired, + themeRef: PropTypes.object.isRequired, +}; + export default Tour; diff --git a/site/src/inspector_panel/InfoToolTip.jsx b/site/src/inspector_panel/InfoToolTip.jsx index e29e0514..d284911a 100644 --- a/site/src/inspector_panel/InfoToolTip.jsx +++ b/site/src/inspector_panel/InfoToolTip.jsx @@ -1,3 +1,4 @@ +import PropTypes from "prop-types"; import Floater from "react-floater"; import InfoIcon from "../icons/icon-info.svg?react"; import { useState } from "react"; @@ -45,4 +46,10 @@ function InfoToolTip({ mode, target, content }) { ); } +InfoToolTip.propTypes = { + mode: PropTypes.string.isRequired, + target: PropTypes.string.isRequired, + content: PropTypes.string.isRequired, +}; + export default InfoToolTip; diff --git a/site/src/inspector_panel/InspectorPanel.jsx b/site/src/inspector_panel/InspectorPanel.jsx index ed939608..aae3ec74 100644 --- a/site/src/inspector_panel/InspectorPanel.jsx +++ b/site/src/inspector_panel/InspectorPanel.jsx @@ -1,6 +1,6 @@ import PropTypes from "prop-types"; import "./InspectorPanel.css"; -import { getThemeConfig, isKnownTheme } from "./config/ThemeRegistry"; +import { getThemeConfig } from "./config/ThemeRegistry"; import { processActiveFeature } from "./utils/EntityProcessor"; import { extractPanelTitle } from "./utils/PanelTitleExtractor"; import PanelHeader from "./components/PanelHeader"; @@ -76,6 +76,11 @@ function InspectorPanel({ } InspectorPanel.propTypes = { - entity: PropTypes.object, + mode: PropTypes.string.isRequired, + setFeatures: PropTypes.func.isRequired, + activeThemes: PropTypes.array.isRequired, + setActiveThemes: PropTypes.func.isRequired, + activeFeature: PropTypes.object, + setActiveFeature: PropTypes.func.isRequired, }; export default InspectorPanel; diff --git a/site/src/inspector_panel/NestedPropertyRow.jsx b/site/src/inspector_panel/NestedPropertyRow.jsx index 083d7df3..7c3e24b1 100644 --- a/site/src/inspector_panel/NestedPropertyRow.jsx +++ b/site/src/inspector_panel/NestedPropertyRow.jsx @@ -1,6 +1,7 @@ +import PropTypes from "prop-types"; import "./NestedPropertyRow.css"; -function NestedPropertyRow({ entity, mode, propertyName, expectedProperties = [] }) { +function NestedPropertyRow({ entity, propertyName, expectedProperties = [] }) { if (!entity[propertyName]) { return null; } @@ -71,13 +72,13 @@ function NestedPropertyRow({ entity, mode, propertyName, expectedProperties = [] // Handle objects if (typeof value === 'object') { - const entries = Object.entries(value).filter(([key, val]) => val != null); + const entries = Object.entries(value).filter(([, val]) => val != null); if (entries.length === 0) return '{}'; - return entries.map(([key, val]) => ( -
+ return entries.map(([objKey, val]) => ( +
- {key}: + {objKey}: {' '} {renderValueContent(val)}
@@ -188,12 +189,12 @@ function NestedPropertyRow({ entity, mode, propertyName, expectedProperties = [] // Render any remaining properties (excluding null values) Object.entries(data) - .filter(([key]) => !processedKeys.has(key)) - .filter(([key, value]) => value != null) - .forEach(([key, value]) => { + .filter(([objKey]) => !processedKeys.has(objKey)) + .filter(([, value]) => value != null) + .forEach(([objKey, value]) => { renderedProperties.push( -

- {key}: +

+ {objKey}: {renderValue(value)}

); @@ -224,4 +225,10 @@ function NestedPropertyRow({ entity, mode, propertyName, expectedProperties = [] ); } +NestedPropertyRow.propTypes = { + entity: PropTypes.object.isRequired, + propertyName: PropTypes.string.isRequired, + expectedProperties: PropTypes.array, +}; + export default NestedPropertyRow; diff --git a/site/src/inspector_panel/SourcesRow.jsx b/site/src/inspector_panel/SourcesRow.jsx index 1503262c..430a47ab 100644 --- a/site/src/inspector_panel/SourcesRow.jsx +++ b/site/src/inspector_panel/SourcesRow.jsx @@ -1,3 +1,4 @@ +import PropTypes from "prop-types"; import InfoToolTip from "./InfoToolTip"; import "./SourcesRow.css"; @@ -92,4 +93,10 @@ function SourcesRow({ entity, mode, tips }) { ); } +SourcesRow.propTypes = { + entity: PropTypes.object.isRequired, + mode: PropTypes.string.isRequired, + tips: PropTypes.object.isRequired, +}; + export default SourcesRow; diff --git a/site/src/inspector_panel/TableRow.jsx b/site/src/inspector_panel/TableRow.jsx index d96dccee..c8f9f3e1 100644 --- a/site/src/inspector_panel/TableRow.jsx +++ b/site/src/inspector_panel/TableRow.jsx @@ -1,7 +1,7 @@ import PropTypes from "prop-types"; import "./TableRow.css"; -function TableRow({ mode, table_key, entity, indented = false }) { +function TableRow({ table_key, entity, indented = false }) { // Function to check if a value is a URL const isURL = (value) => { if (!value || typeof value !== 'string') return false; @@ -68,13 +68,13 @@ function TableRow({ mode, table_key, entity, indented = false }) { // Handle objects if (typeof value === 'object') { - const entries = Object.entries(value).filter(([key, val]) => val != null && val !== "null"); + const entries = Object.entries(value).filter(([, val]) => val != null && val !== "null"); if (entries.length === 0) return '{}'; - return entries.map(([key, val]) => ( -
+ return entries.map(([objKey, val]) => ( +
- {key}: + {objKey}: {' '} {renderValueContent(val)}
@@ -159,7 +159,6 @@ function TableRow({ mode, table_key, entity, indented = false }) { } TableRow.propTypes = { - mode: PropTypes.string, table_key: PropTypes.string.isRequired, entity: PropTypes.object.isRequired, indented: PropTypes.bool, diff --git a/site/src/inspector_panel/ThemePanel.jsx b/site/src/inspector_panel/ThemePanel.jsx index b7435e24..2ed97f3a 100644 --- a/site/src/inspector_panel/ThemePanel.jsx +++ b/site/src/inspector_panel/ThemePanel.jsx @@ -1,10 +1,10 @@ +import PropTypes from "prop-types"; import TableRow from "./TableRow"; import "./ThemePanel.css"; import IndentIcon from "../icons/icon-indent.svg?react"; import InfoToolTip from "./InfoToolTip"; import PushPinIcon from "@mui/icons-material/PushPin"; import PushPinOutlinedIcon from "@mui/icons-material/PushPinOutlined"; -import ThemeIcon from "./ThemeIcon"; import SourcesRow from "./SourcesRow.jsx"; import NestedPropertyRow from "./NestedPropertyRow.jsx"; @@ -18,6 +18,7 @@ const sharedProperties = [ "categories", "subtype", "class", + "subclass", "version", ]; @@ -121,13 +122,11 @@ function ThemePanel({ mode, entity, tips, activeThemes, setActiveThemes }) { )} @@ -152,7 +151,7 @@ function ThemePanel({ mode, entity, tips, activeThemes, setActiveThemes }) { .filter((key) => !sharedProperties.includes(key)) .filter((key) => entity[key] != null && entity[key] !== "null") .map((key) => ( - + ))} @@ -160,4 +159,12 @@ function ThemePanel({ mode, entity, tips, activeThemes, setActiveThemes }) { ); } +ThemePanel.propTypes = { + mode: PropTypes.string.isRequired, + entity: PropTypes.object.isRequired, + tips: PropTypes.object.isRequired, + activeThemes: PropTypes.array.isRequired, + setActiveThemes: PropTypes.func.isRequired, +}; + export default ThemePanel; diff --git a/site/src/inspector_panel/utils/PropertyOrderer.js b/site/src/inspector_panel/utils/PropertyOrderer.js index 0c90cedc..2349708d 100644 --- a/site/src/inspector_panel/utils/PropertyOrderer.js +++ b/site/src/inspector_panel/utils/PropertyOrderer.js @@ -18,7 +18,7 @@ export const createOrderedKeys = (entity) => { processedKeys.add(key); // If this is "class" and "subclass" exists, add subclass right after - if (key === "class" && entity.hasOwnProperty("subclass")) { + if (key === "class" && Object.prototype.hasOwnProperty.call(entity, "subclass")) { orderedKeys.push({ key: "subclass", indented: true }); processedKeys.add("subclass"); } diff --git a/site/src/nav/DownloadButton.jsx b/site/src/nav/DownloadButton.jsx index 5d131f5c..cc612c6f 100644 --- a/site/src/nav/DownloadButton.jsx +++ b/site/src/nav/DownloadButton.jsx @@ -28,7 +28,7 @@ function DownloadButton({ mode, zoom, setZoom, visibleTypes}) { myMap.getBounds(); setZoom(myMap.getZoom()); } - }, [myMap]); + }, [myMap, setZoom]); const handleDownloadClick = async () => { @@ -219,4 +219,11 @@ function DownloadButton({ mode, zoom, setZoom, visibleTypes}) { ); } +DownloadButton.propTypes = { + mode: PropTypes.string.isRequired, + zoom: PropTypes.number.isRequired, + setZoom: PropTypes.func.isRequired, + visibleTypes: PropTypes.array.isRequired, +}; + export default DownloadButton; diff --git a/site/src/nav/Header.jsx b/site/src/nav/Header.jsx index 872afed9..2a324a17 100644 --- a/site/src/nav/Header.jsx +++ b/site/src/nav/Header.jsx @@ -30,6 +30,9 @@ export default function Header({ zoom, mode, setMode, setZoom, visibleTypes}) { } Header.propTypes = { + zoom: PropTypes.number.isRequired, mode: PropTypes.string.isRequired, setMode: PropTypes.func.isRequired, + setZoom: PropTypes.func.isRequired, + visibleTypes: PropTypes.array.isRequired, }; diff --git a/site/src/navigator/Navigator.jsx b/site/src/navigator/Navigator.jsx index e3626ac4..99101b13 100644 --- a/site/src/navigator/Navigator.jsx +++ b/site/src/navigator/Navigator.jsx @@ -1,25 +1,12 @@ +import PropTypes from "prop-types"; import "./Navigator.css"; import CloseIcon from "@mui/icons-material/Close"; import ArrowForwardIosIcon from "@mui/icons-material/ArrowForwardIos"; import FlightTakeoffIcon from "@mui/icons-material/FlightTakeoff"; import { useMap } from "react-map-gl/maplibre"; import { tours } from "./NavigatorConfig"; -import { useState, useEffect } from "react"; -export function useNavigatorState(initialOpen = false) { - const [navigatorOpen, setNavigatorOpen] = useState(() => { - const stored = localStorage.getItem("navigatorOpen"); - return stored !== null ? JSON.parse(stored) : !initialOpen; - }); - - useEffect(() => { - localStorage.setItem("navigatorOpen", JSON.stringify(navigatorOpen)); - }, [navigatorOpen]); - - return [navigatorOpen, setNavigatorOpen]; -} - -function Navigator({ open, setOpen, map, setVisibleTypes, setActiveThemes }) { +function Navigator({ open, setOpen, setVisibleTypes, setActiveThemes }) { const { myMap } = useMap(); const handleTourSelect = (tourId) => { @@ -44,7 +31,7 @@ function Navigator({ open, setOpen, map, setVisibleTypes, setActiveThemes }) {
- We've picked a few spots around the world you might be interested + We've picked a few spots around the world you might be interested in seeing.
@@ -75,4 +62,11 @@ function Navigator({ open, setOpen, map, setVisibleTypes, setActiveThemes }) { ); } +Navigator.propTypes = { + open: PropTypes.bool.isRequired, + setOpen: PropTypes.func.isRequired, + setVisibleTypes: PropTypes.func.isRequired, + setActiveThemes: PropTypes.func.isRequired, +}; + export default Navigator; diff --git a/site/src/navigator/useNavigatorState.js b/site/src/navigator/useNavigatorState.js new file mode 100644 index 00000000..724855fd --- /dev/null +++ b/site/src/navigator/useNavigatorState.js @@ -0,0 +1,14 @@ +import { useState, useEffect } from "react"; + +export function useNavigatorState(initialOpen = false) { + const [navigatorOpen, setNavigatorOpen] = useState(() => { + const stored = localStorage.getItem("navigatorOpen"); + return stored !== null ? JSON.parse(stored) : !initialOpen; + }); + + useEffect(() => { + localStorage.setItem("navigatorOpen", JSON.stringify(navigatorOpen)); + }, [navigatorOpen]); + + return [navigatorOpen, setNavigatorOpen]; +} diff --git a/site/vite.config.js b/site/vite.config.js index 2c88fda6..31d38ea7 100644 --- a/site/vite.config.js +++ b/site/vite.config.js @@ -1,3 +1,4 @@ +/* eslint-env node */ import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' import svgr from 'vite-plugin-svgr'